**The Art of Using std::variant for Polymorphic Data Storage**

std::variant is a type-safe union introduced in C++17 that allows a variable to hold one of several specified types at any given time. Unlike traditional unions, which lack type safety and require manual management of which member is active, std::variant keeps track of the currently held type internally, enabling safer and more expressive code. In this article we’ll explore the key features of std::variant, how to use it effectively, and some common pitfalls to avoid.


1. Basic Construction and Access

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

int main() {
    std::variant<int, double, std::string> v;

    v = 42;                          // Holds an int
    std::cout << std::get<int>(v) << '\n';

    v = 3.14;                        // Now holds a double
    std::cout << std::get<double>(v) << '\n';

    v = std::string("Hello C++");     // Now holds a string
    std::cout << std::get<std::string>(v) << '\n';

    return 0;
}
  • `std::get (v)` retrieves the value, throwing `std::bad_variant_access` if the wrong type is requested.
  • `std::holds_alternative (v)` returns a boolean indicating the current active type.
  • v.index() returns the zero‑based index of the active type in the type list.

2. Visitor Pattern with std::visit

The most powerful way to operate on a std::variant is via visitors. A visitor is simply a function object that overloads operator() for each possible type.

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

void print_variant(const std::variant<int, double, std::string>& v) {
    std::visit([](auto&& arg) {
        std::cout << "Value: " << arg << '\n';
    }, v);
}

Because the lambda is a generic function template, it accepts any type that the variant might hold. For more complex logic, you can supply a struct with overloaded operator():

struct SumVisitor {
    double operator()(int i) const { return i; }
    double operator()(double d) const { return d; }
    double operator()(const std::string&) const { return 0.0; }
};

double sum_of_variant(const std::variant<int, double, std::string>& v) {
    return std::visit(SumVisitor{}, v);
}

3. Using std::variant as a Polymorphic Container

Suppose you want a container that holds different types of shapes but only one shape per element. With std::variant, you can avoid raw pointers and dynamic allocation.

#include <variant>
#include <vector>
#include <memory>

struct Circle { double radius; };
struct Square { double side; };
struct Triangle { double base, height; };

using Shape = std::variant<Circle, Square, Triangle>;

int main() {
    std::vector <Shape> shapes;
    shapes.emplace_back(Circle{5.0});
    shapes.emplace_back(Square{3.0});
    shapes.emplace_back(Triangle{4.0, 6.0});

    for (const auto& shape : shapes) {
        std::visit([](auto&& s){
            using T = std::decay_t<decltype(s)>;
            if constexpr (std::is_same_v<T, Circle>) {
                std::cout << "Circle with radius " << s.radius << '\n';
            } else if constexpr (std::is_same_v<T, Square>) {
                std::cout << "Square with side " << s.side << '\n';
            } else if constexpr (std::is_same_v<T, Triangle>) {
                std::cout << "Triangle base " << s.base << " height " << s.height << '\n';
            }
        }, shape);
    }
}

This pattern keeps the benefits of static typing while offering the flexibility of heterogeneous collections.


4. Common Pitfalls

Issue Why It Happens Fix
Copying a variant with non‑copyable alternatives std::variant’s copy constructor requires all alternatives to be copy‑constructible. Use std::move or switch to std::optional<std::variant<...>> to handle move‑only types.
Using std::get without checking Throws std::bad_variant_access if the wrong type is requested. Use `std::holds_alternative
(v)` first or catch the exception.
Index out of range Accessing v[index] without ensuring the index is valid. Use v.index() to check or rely on visitors.
Complex visitor with overloads Ambiguity or overload resolution errors. Define a struct with explicit overloads or use std::overloaded.

5. Extending with std::monostate

std::monostate is a type you can include in a variant to represent an “empty” state, similar to std::optional but with a guaranteed index of . This is handy when you need an optional value that is still part of a variant.

using MaybeInt = std::variant<std::monostate, int>;

MaybeInt maybe = 42;          // Holds int
MaybeInt empty;               // Holds monostate

if (std::holds_alternative<std::monostate>(empty)) {
    std::cout << "No value\n";
}

6. When to Use std::variant

Scenario Reason
Representing a JSON value JSON can be null, boolean, number, string, array, or object. std::variant models this elegantly.
State machines Each state can be a distinct type; a variant holds the current state.
Heterogeneous collections A vector of shapes, commands, or AST nodes.
Return types Functions that may return different result types (e.g., int or std::string error).

7. Conclusion

std::variant provides a type‑safe, efficient way to store values of different types without the overhead and danger of raw unions or dynamic polymorphism. By mastering visitors, careful construction, and the common pitfalls, you can write cleaner, safer C++ code that leverages the full power of modern type system features. Happy coding!

发表评论