**How to Use `std::variant` for Sum Types in Modern C++**

std::variant is a type-safe union that was introduced in C++17. It lets you store one value from a set of types in a single variable, much like a discriminated union in other languages. This feature is incredibly useful for modeling sum types, handling error states, or simply reducing the need for manual type checks.


1. Basic Declaration and Initialization

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

int main() {
    std::variant<int, std::string> data = 42;           // holds an int
    std::variant<int, std::string> text = std::string("hello");

    std::cout << "int value: " << std::get<int>(data) << '\n';
    std::cout << "string value: " << std::get<std::string>(text) << '\n';
}
  • The variant is a template that takes an arbitrary number of types.
  • The contained value can be retrieved with `std::get (variant)`. If the wrong type is requested, a `std::bad_variant_access` exception is thrown.

2. Visiting – The Safe Way to Handle All Cases

Instead of manually checking the active type, use std::visit with a lambda or a functor.

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

int main() {
    std::variant<int, std::string> data = "world";

    std::visit([](auto&& val) {
        using T = std::decay_t<decltype(val)>;
        if constexpr (std::is_same_v<T, int>) {
            std::cout << "int: " << val << '\n';
        } else if constexpr (std::is_same_v<T, std::string>) {
            std::cout << "string: " << val << '\n';
        }
    }, data);
}

std::visit will call the visitor with the currently stored value, allowing you to write type‑agnostic code.


3. Common Pitfalls

  1. Ambiguous Overloads
    If two types in the variant can be implicitly converted from the same expression, the compiler cannot decide which to use.

    std::variant<int, long> v = 5; // ambiguous (int or long)

    Use explicit construction: std::variant<int, long> v = int{5};

  2. Copying Variants with Large Types
    std::variant stores all alternatives in the same memory block. If one alternative is very large, consider storing it by pointer or using std::unique_ptr inside the variant.

  3. Missing Default Case in std::visit
    If you forget a case, the visitor will still compile because the lambda is generic. But runtime errors can happen. Use std::visit with a std::variant that contains all possible types you intend to handle.


4. Practical Use‑Case: Error Handling

Replace classic std::pair<bool, T> or custom error enums with a variant.

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

struct Success {
    int result;
};

struct Error {
    std::string message;
};

using Result = std::variant<Success, Error>;

Result compute(int x) {
    if (x < 0)
        return Error{"Negative input"};
    return Success{x * 2};
}

int main() {
    auto res = compute(5);
    std::visit([](auto&& val){
        using T = std::decay_t<decltype(val)>;
        if constexpr (std::is_same_v<T, Success>) {
            std::cout << "Success: " << val.result << '\n';
        } else {
            std::cout << "Error: " << val.message << '\n';
        }
    }, res);
}

The variant cleanly encodes the “either” nature of the result, making the API easier to read and less error‑prone.


5. Extending with std::visit and Overload Sets

You can simplify visitors by combining overloaded lambdas:

template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template<class... Ts> overloaded(Ts...)->overloaded<Ts...>;

std::variant<int, std::string, double> v = 3.14;

std::visit(overloaded{
    [](int i){ std::cout << "int: " << i << '\n'; },
    [](const std::string& s){ std::cout << "string: " << s << '\n'; },
    [](double d){ std::cout << "double: " << d << '\n'; }
}, v);

This pattern keeps your visitor code concise and readable.


6. Performance Considerations

  • In‑place Storage: std::variant keeps all alternatives in a single buffer. If alternatives are large, consider using pointers or std::unique_ptr.
  • Small-Object Optimization: For small types, the overhead is minimal; the variant is typically as fast as a union with manual tag handling.
  • Exception Safety: std::variant guarantees no resource leaks when the active alternative is swapped or destroyed, as long as the contained types are themselves exception‑safe.

7. Bottom Line

std::variant gives you a clean, type‑safe way to handle values that can be one of several types. It replaces many ad‑hoc approaches (tagged unions, unions with enums, or error‑code integers) and integrates seamlessly with C++’s pattern‑matching via std::visit. Embrace it for safer, clearer code when you need a sum type.

Happy coding!

发表评论