**How to Implement a Type‑Safe Visitor Using std::variant in C++17?**

std::variant (introduced in C++17) is a powerful type‑safe union that can hold one value from a set of specified types. When combined with a visitor pattern, it becomes easy to write code that operates differently based on the actual type stored in the variant, without resorting to manual type checks or dynamic_cast. This article shows how to write a generic visitor helper and demonstrates its use with a simple shape hierarchy.


1. The Problem

Suppose you have a set of geometric shapes:

struct Circle   { double radius; };
struct Rectangle{ double width, height; };
struct Triangle { double base, height; };

You want a function that can print the area of any shape, but you only want to write the logic once for each concrete type. A naive solution uses polymorphism:

class Shape { virtual double area() const = 0; /* ... */ };

However, polymorphism requires dynamic allocation and virtual tables, which can be avoided if all shape types are known at compile time. std::variant offers a compile‑time type‑safe alternative.


2. std::variant Basics

#include <variant>
#include <iostream>
#include <cmath>

using ShapeVariant = std::variant<Circle, Rectangle, Triangle>;

ShapeVariant can store any one of the three structs. You create it like this:

ShapeVariant shape = Circle{ 5.0 };           // a circle of radius 5
ShapeVariant shape = Rectangle{ 3.0, 4.0 };   // a rectangle 3x4
ShapeVariant shape = Triangle{ 3.0, 4.0 };    // a triangle base 3, height 4

3. Visiting a Variant

std::visit takes a visitor object (any callable) and a variant, and calls the visitor with the currently held type. The challenge is to write a visitor that can call the correct area function for each shape.

A minimal visitor could look like this:

double areaVisitor(const Circle& c)          { return M_PI * c.radius * c.radius; }
double areaVisitor(const Rectangle& r)       { return r.width * r.height; }
double areaVisitor(const Triangle& t)        { return 0.5 * t.base * t.height; }

Then use:

double area = std::visit(areaVisitor, shape);

But writing a separate overload for each type can become cumbersome for large unions. A more general solution uses a lambda pack or a helper struct.


4. A Generic Visitor Helper

We can create a variant_visitor struct that aggregates multiple callables:

template<class... Ts>
struct variant_visitor : Ts... {
    using Ts::operator()...;      // bring all operator() into scope
    constexpr variant_visitor(Ts... ts) : Ts(ts)... {}
};

template<class... Ts>
variant_visitor(Ts...) -> variant_visitor<Ts...>;

Now you can pass a tuple of lambdas to std::visit:

auto visitor = variant_visitor{
    [](const Circle& c)          { return M_PI * c.radius * c.radius; },
    [](const Rectangle& r)       { return r.width * r.height; },
    [](const Triangle& t)        { return 0.5 * t.base * t.height; }
};

double area = std::visit(visitor, shape);

Because variant_visitor inherits from all the lambda types, the operator() overloads are combined into a single callable that matches any of the stored types. The compiler deduces the return type automatically (here it is double).


5. Full Example

#include <variant>
#include <iostream>
#include <cmath>

// ---------- Shape definitions ----------
struct Circle   { double radius; };
struct Rectangle{ double width, height; };
struct Triangle { double base, height; };

// ---------- Variant type ----------
using ShapeVariant = std::variant<Circle, Rectangle, Triangle>;

// ---------- Generic visitor helper ----------
template<class... Ts>
struct variant_visitor : Ts... {
    using Ts::operator()...;
    constexpr variant_visitor(Ts... ts) : Ts(ts)... {}
};

template<class... Ts>
variant_visitor(Ts...) -> variant_visitor<Ts...>;

// ---------- Main ----------
int main() {
    ShapeVariant shapes[] = {
        Circle{5.0},
        Rectangle{3.0, 4.0},
        Triangle{3.0, 4.0}
    };

    for (const auto& shape : shapes) {
        double area = std::visit(
            variant_visitor{
                [](const Circle& c)          { return M_PI * c.radius * c.radius; },
                [](const Rectangle& r)       { return r.width * r.height; },
                [](const Triangle& t)        { return 0.5 * t.base * t.height; }
            },
            shape
        );

        std::cout << "Area: " << area << '\n';
    }
}

Compile with a C++17 compiler:

g++ -std=c++17 -O2 -Wall variant_example.cpp -o variant_example
./variant_example

Output

Area: 78.5398
Area: 12
Area: 6

6. Why Use This Pattern?

Benefit Explanation
Type safety The compiler guarantees that every possible type is handled; missing a type will produce a compile‑time error.
No dynamic allocation All objects are stored on the stack or in place; no heap allocation or virtual tables.
Extensible Adding a new shape only requires adding a new variant alternative and a lambda overload.
Readability The visitor keeps all shape‑specific logic in one place, reducing boilerplate.

7. Tips & Tricks

  • Return type deduction: std::visit deduces the return type from the visitor. All lambdas must return the same type (or a type convertible to a common type).
  • Ref‑qualifiers: If you need to modify the variant’s contents, pass it by reference: auto visitor = variant_visitor{ [&](Circle& c){ c.radius *= 2; return 0.0; }, ... };
  • Custom visitor structs: For more complex visitors, define a struct with multiple operator() overloads instead of lambdas.
  • Combining with std::optional: Often, you’ll want to store an optional shape; `std::optional ` is straightforward to use with the same pattern.

8. Conclusion

std::variant and std::visit together provide a clean, type‑safe, and efficient way to implement visitor logic without relying on inheritance or dynamic memory. By wrapping a set of overloaded lambdas in a variant_visitor, you get a reusable and expressive tool that scales well with the number of types in your variant. Whether you’re building a graphics engine, a compiler front‑end, or a simple calculator, this pattern will simplify your code and make it safer.

发表评论