What is std::variant and how does it compare to union?

std::variant is a type-safe union introduced in C++17 that allows you to store one of several predefined types in a single variable while preserving type information at runtime. Unlike a C-style union, std::variant provides a rich set of operations for construction, destruction, visitation, and querying, and it guarantees that only the active member is alive. It also offers strong exception safety guarantees and integrates seamlessly with the standard library.

1. Basic definition

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

using Value = std::variant<int, double, std::string>;

int main() {
    Value v = 42;           // holds an int
    v = 3.14;               // now holds a double
    v = std::string{"hello"}; // now holds a std::string

    std::cout << std::get<std::string>(v) << '\n';
}

The type Value can hold an int, a double, or a std::string. At any moment, only one of these types is active.

2. Construction and assignment

std::variant supports:

  • Direct initialization from any of its alternatives.
  • Copy/move construction and assignment.
  • Default construction, which initializes the first alternative (int in the example above) to its default value.
Value a;                // a holds int{0}
Value b = 5;            // holds int{5}
Value c = std::in_place_index <2>, std::string{"world"}; // holds std::string{"world"}

The std::in_place_index or std::in_place_type constructors allow you to specify which alternative to construct.

3. Visiting

The most powerful feature of std::variant is the std::visit function. It accepts a visitor (a callable object) and applies it to the active alternative.

struct Printer {
    void operator()(int i) const { std::cout << "int: " << i << '\n'; }
    void operator()(double d) const { std::cout << "double: " << d << '\n'; }
    void operator()(const std::string& s) const { std::cout << "string: " << s << '\n'; }
};

Value v = 3.14;
std::visit(Printer{}, v); // prints "double: 3.14"

std::visit ensures that the correct overload is called, avoiding the need for manual type checks.

4. Querying the active alternative

  • `std::holds_alternative (v)` returns `true` if `v` currently holds type `T`.
  • `std::get (v)` returns the stored value of type `T`; it throws `std::bad_variant_access` if `T` is not the active type.
  • `std::get_if (&v)` returns a pointer to the stored value or `nullptr` if not active.
if (std::holds_alternative<std::string>(v)) {
    std::cout << "string value: " << std::get<std::string>(v) << '\n';
}

5. Comparison with C-style union

Feature std::variant C-style union
Type safety Guaranteed; only one active member. Manual; potential undefined behavior.
Construction/destruction Automatically handles all types. Requires manual constructors/destructors.
Exception safety Strong guarantee. Risk of resource leaks.
Runtime type information index() returns current type index. No built-in runtime info.
Visitation std::visit provides compile-time dispatch. No standard visitor pattern.
Size May be larger due to additional metadata. Minimal, just the largest member.

6. Practical use cases

  1. Parsing heterogeneous data: When parsing JSON, a node may contain a string, number, boolean, null, array, or object. std::variant cleanly represents these possibilities.
  2. Event handling: An event system where events can be of multiple distinct types can use a variant to hold any event object.
  3. State representation: Finite state machines where a state can be one of several structs can use a variant to hold the current state.

7. Performance considerations

While std::variant incurs a small runtime overhead due to its internal bookkeeping (index and exception safety mechanisms), this cost is often negligible compared to the clarity and safety benefits. For performance-critical code where memory layout matters, a traditional union combined with manual tagging might still be preferable.

8. Summary

std::variant is a powerful, type-safe alternative to unions, providing automatic lifetime management, visitation, and robust type queries. Its integration with the standard library makes it an indispensable tool for modern C++ developers who need to work with values that can be one of several types without sacrificing safety or expressiveness.

发表评论