C/C++ Arena

Step 2 of 7

std::variant and std::visit

Sometimes a value can be one of several different kinds: a JSON value is a number, a string or a list; a shape is a circle or a rectangle; a message is a login, a chat line or a logout. In C you'd use a union plus a tag field and hope nobody reads the wrong member.

std::variant<A, B, C> (from <variant>) holds exactly one of its types at a time and always knows which one. Reading the wrong one is caught instead of silently producing garbage.

#include <iostream>
#include <string>
#include <variant>
#include <vector>

struct Circle { double r; };
struct Square { double side; };
using Shape = std::variant<Circle, Square>;

double area(const Shape& s) {
    if (const Circle* c = std::get_if<Circle>(&s)) return 3.14 * c->r * c->r;
    return std::get<Square>(s).side * std::get<Square>(s).side;
}

int main() {
    std::vector<Shape> shapes = {Circle{1}, Square{3}};
    for (const auto& s : shapes) {
        std::cout << (std::holds_alternative<Circle>(s) ? "circle " : "square ") << area(s) << "\n";
    }

    std::variant<int, std::string> cell = 42;
    cell = std::string("hello");               // now it holds a string
    std::visit([](const auto& x) { std::cout << "cell: " << x << "\n"; }, cell);
    std::cout << "index " << cell.index() << "\n";
}
circle 3.14
square 9
cell: hello
index 1

Ways to look inside

Tool Use
std::holds_alternative<T>(v) true if it currently holds a T
std::get<T>(v) the T inside (only when it holds one)
std::get_if<T>(&v) a pointer to the T, or nullptr if it holds something else
std::visit(f, v) calls f with whatever is inside
v.index() which alternative, counting from 0

std::get_if combines the check and the access, which is why the area function uses it. In std::visit, a lambda with an auto parameter is compiled once per alternative, so it handles every type.

std::get with the wrong type throws std::bad_variant_access. When a wrong type is a normal possibility rather than a bug, check first with holds_alternative or use get_if.

Your turn: an Event is either a Kill{attacker, victim}, a Plant{site} or a RoundEnd{winner}. Write std::string describe(const Event& e) returning "A killed B", "bomb planted at X" or "round to W". Use std::holds_alternative/std::get, or std::get_if.

Previous: std::optional Next: std::string_view