Skip to content
C++
Library
since C++17
Intermediate

std::variant

Type-safe tagged union holding exactly one of a fixed set of alternative types, introduced in C++17 with no heap allocation.

std::variant<Ts...>since C++17

A type-safe discriminated union that holds exactly one value from a compile-time-fixed set of alternative types, stored inline with no heap allocation.

Overview

std::variant (header <variant>) models a sum type β€” the moral equivalent of a union with an embedded discriminant tag, exception-safety guarantees, and a visitation API. It is the right tool when a value can be one of several unrelated types known at compile time, and you need exhaustive, type-safe handling of each alternative.

Compared to alternatives:

  • Raw union β€” no tag, no safety, manual lifetime management; use only when ABI or performance demands it.
  • Inheritance + virtual dispatch β€” requires heap allocation and a pointer, supports an open type set, not a fixed one.
  • std::any β€” type-erased open set, small-buffer optimized but potentially heap-backing, runtime-only type check with no exhaustiveness guarantee.

std::variant stores its value inline (storage size = max(sizeof(Ts)...) rounded up for alignment, plus a discriminant). It makes no heap allocations and has full constexpr support starting in C++20.

Syntax

cpp
#include <variant>

// Default-constructs the first alternative
std::variant<int, double, std::string> v;   // holds int{0}

// Assign from any alternative type
v = 3.14;           // now holds double
v = std::string{"hello"};  // now holds string

// In-place construction β€” avoids a temporary, C++17
std::variant<std::string, std::vector<int>> a{
    std::in_place_type<std::vector<int>>, 5, 0   // vector of five zeros
};
std::variant<std::string, std::vector<int>> b{
    std::in_place_index<0>, 3, 'x'               // string(3, 'x') = "xxx"
};

// Emplace after construction
v.emplace<std::string>("rebuilt");   // by type
v.emplace<2>("rebuilt again");       // by index (same as string here)

Member functions (C++17)

MemberDescription
v.index()Zero-based index of the active alternative; variant_npos if valueless
v.valueless_by_exception()true if the variant holds no value due to a thrown exception during assignment
v.emplace<T>(args...)Destroy current value, construct T in-place
v.emplace<I>(args...)Destroy current value, construct alternative at index I in-place

Free functions (C++17)

FunctionReturnsThrows
std::get<T>(v)T& / const T&std::bad_variant_access if wrong type
std::get<I>(v)Reference to alternative Istd::bad_variant_access if wrong index
std::get_if<T>(&v)T* or nullptrNever
std::get_if<I>(&v)Pointer to alternative I or nullptrNever
std::holds_alternative<T>(v)boolNever
std::visit(f, v...)Deduced from fstd::bad_variant_access if any arg is valueless

std::visit became constexpr in C++20. C++23 added std::visit<R>(f, v...) to specify the return type explicitly when branches return different-but-convertible types.

Examples

The overloaded pattern β€” exhaustive visitation

The standard idiom for multi-arm visitation. Define it once in a shared header.

cpp
// C++17: explicit deduction guide required
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };

template<class... Ts>        // deduction guide β€” needed in C++17
overloaded(Ts...) -> overloaded<Ts...>;
// In C++20 the deduction guide is auto-generated for aggregates

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

std::visit(overloaded{
    [](int n)               { /* handle int    */ },
    [](double d)            { /* handle double */ },
    [](const std::string& s){ /* handle string */ },
}, v);

AST expression evaluation

Recursive variants require a pointer indirection to avoid an incomplete-type instantiation error. std::unique_ptr is the idiomatic wrapper.

cpp
#include <variant>
#include <memory>
#include <string>
#include <unordered_map>
#include <cstdint>

struct Literal  { std::int64_t value; };
struct Variable { std::string  name;  };
struct Add; struct Mul;  // forward-declared; Expr stores owning pointers

using Expr = std::variant<
    Literal,
    Variable,
    std::unique_ptr<Add>,
    std::unique_ptr<Mul>
>;

struct Add { Expr lhs, rhs; };
struct Mul { Expr lhs, rhs; };

using Env = std::unordered_map<std::string, std::int64_t>;

std::int64_t evaluate(const Expr& e, const Env& env) {
    return std::visit(overloaded{
        [](const Literal& l)                    { return l.value; },
        [&](const Variable& v)                  { return env.at(v.name); },
        [&](const std::unique_ptr<Add>& p)      {
            return evaluate(p->lhs, env) + evaluate(p->rhs, env);
        },
        [&](const std::unique_ptr<Mul>& p)      {
            return evaluate(p->lhs, env) * evaluate(p->rhs, env);
        },
    }, e);
}

Error handling without exceptions

cpp
#include <variant>
#include <string>
#include <charconv>

template<typename T, typename E = std::string>
using Result = std::variant<T, E>;

Result<int> parse_int(std::string_view sv) {
    int out{};
    auto [ptr, ec] = std::from_chars(sv.data(), sv.data() + sv.size(), out);
    if (ec != std::errc{})
        return std::string{"parse error: "} + std::string{sv};
    return out;
}

// At call site:
std::visit(overloaded{
    [](int value)              { /* use parsed integer */ },
    [](const std::string& err) { /* log or propagate   */ },
}, parse_int("42abc"));

For strictly binary OK/error results, prefer std::expected<T, E> (C++23) β€” it has a richer API (value(), error(), and_then, transform) and communicates intent more clearly. Use std::variant when there are three or more distinct outcome types.

std::monostate β€” explicit empty state

cpp
// Widget and Connection are not default-constructible
struct Widget    { explicit Widget(int id); };
struct Connection{ explicit Connection(std::string url); };

// monostate as first alternative gives the variant a default-constructible "empty" state
std::variant<std::monostate, Widget, Connection> resource;
// resource.index() == 0 β€” holds monostate, not valueless_by_exception
resource.emplace<Widget>(42);

Multi-variant visitation

std::visit accepts any number of variants. The visitor is instantiated for every combination of active types, so the instantiation count is |T1| Γ— |T2| Γ— …. Keep arity small.

cpp
std::variant<int, double> a = 1;
std::variant<int, double> b = 2.5;

double sum = std::visit([](auto x, auto y) -> double {
    return static_cast<double>(x) + static_cast<double>(y);
}, a, b);

constexpr variant β€” C++20

cpp
constexpr std::variant<int, double> pick(bool flag) {   // constexpr: C++20
    return flag ? std::variant<int, double>{42}
                : std::variant<int, double>{3.14};
}
static_assert(std::get<int>(pick(true)) == 42);

Best Practices

Prefer std::visit over holds_alternative / get chains. A sequence of if (holds_alternative<T>(v)) branches is O(n), non-exhaustive by construction, and forces the reader to mentally verify coverage. std::visit is exhaustive by default and typically compiled to a jump table.

Use emplace over assignment for non-trivial types. v = T{args...} constructs a temporary and move-assigns; v.emplace<T>(args...) constructs directly inside the variant's buffer, skipping the temporary.

Keep all alternatives noexcept-move-constructible. This is the single most important correctness property: if moves cannot throw, variant assignment can never produce the valueless_by_exception state (see pitfalls below). Trivially copyable types are always safe.

Unify the overloaded helper in one header. The template is four lines but non-trivial to rediscover. Define it once, name it consistently, and share it across the codebase.

Return consistent types from visitors. All branches must return the same type or types that share a common implicit conversion. When they do not, use std::visit<R> (C++23) to nominate a coercion target rather than scattering static_casts across lambda bodies.

Common Pitfalls

Duplicate alternative types β€” index-only access

cpp
std::variant<int, int> v{std::in_place_index<1>, 99};
std::get<int>(v);    // error: call to 'get' is ambiguous
std::get<1>(v);      // OK: 99

Avoid duplicate types entirely. If the same underlying type carries distinct semantics, wrap it in a named struct.

Surprising implicit conversions during assignment

cpp
std::variant<float, long> v = true;  // assigns float — bool→float preferred by overload resolution
std::variant<int, long>   w = 0;     // assigns int, not long

When the assigned value converts to multiple alternatives, overload resolution picks according to standard conversion rankings, which can be unintuitive. Be explicit: use emplace or cast at the call site.

valueless_by_exception β€” the broken-invariant state

If an assignment's move-construction throws after the old value has been destroyed, the variant enters a hollow state where index() returns variant_npos, every std::get call throws, and std::visit throws.

cpp
struct Fragile {
    Fragile() = default;
    Fragile(Fragile&&) { throw std::runtime_error{"move failed"}; }
};

std::variant<int, Fragile> v = 42;
try { v = Fragile{}; } catch (...) {}  // int destroyed, move threw
assert(v.valueless_by_exception());    // v is now unusable

The fix is architectural: ensure move constructors in variant alternatives are noexcept. The standard guarantees this state cannot arise for variants whose alternatives are all trivially copyable.

Non-identical branch return types

cpp
// Does not compile β€” branches deduce int and double separately
std::visit(overloaded{
    [](int)    { return 1;   },   // deduces int
    [](double) { return 2.0; },   // deduces double
}, v);

// Fix: explicit return type annotation on each lambda
std::visit(overloaded{
    [](int)    -> double { return 1;   },
    [](double) -> double { return 2.0; },
}, v);

// Or use visit<R> β€” C++23
std::visit<double>(overloaded{
    [](int)    { return 1;   },
    [](double) { return 2.0; },
}, v);

std::visit with multiple variants

std::visit accepts two or more variants simultaneously, dispatching on all active types at once. The visitor callable is instantiated for every Cartesian product of alternatives, so compilation cost grows multiplicatively β€” keep arity and alternative counts small.

cpp
// C++17
#include <variant>
#include <string>

using Number = std::variant<int, double>;

Number add(Number a, Number b) {
    return std::visit([](auto x, auto y) -> Number {
        return x + y;   // both operands promoted to common type
    }, a, b);
}

Number result = add(Number{3}, Number{1.5});
// result holds double{4.5}

Combinatorial explosion warning: two variants of 4 alternatives each produce 16 instantiations; three produce 64. Prefer a single variant with a richer alternative set when multi-variant dispatch becomes unwieldy.

cpp
// C++17: two-variant dispatch to implement a binary operation table
using Shape = std::variant<struct Circle, struct Rect>;

bool overlaps(const Shape& a, const Shape& b) {
    return std::visit(overloaded{
        [](const Circle& c1, const Circle& c2) { /* circle-circle */ return true; },
        [](const Circle& c,  const Rect&   r)  { /* circle-rect   */ return true; },
        [](const Rect&   r,  const Circle& c)  { /* rect-circle   */ return true; },
        [](const Rect&   r1, const Rect&   r2) { /* rect-rect     */ return true; },
    }, a, b);
}

Overload trick for visitors

The overloaded helper aggregates multiple lambdas into a single callable that the compiler can use for exhaustive visitation. It works by inheriting operator() from each lambda and letting overload resolution pick the right arm.

cpp
// C++17 β€” deduction guide required
template<class... Ts>
struct overloaded : Ts... { using Ts::operator()...; };

template<class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;
// C++20: deduction guide is generated automatically for aggregate types

The helper is reusable beyond std::variant β€” any callable that dispatches on an argument type benefits from it.

cpp
// C++17: overloaded used outside std::visit
auto printer = overloaded{
    [](int n)               { printf("int: %d\n", n); },
    [](double d)            { printf("double: %f\n", d); },
    [](const std::string& s){ printf("string: %s\n", s.c_str()); },
};

printer(42);           // int: 42
printer(3.14);         // double: 3.140000
printer(std::string{"hi"});  // string: hi

A common mistake is forgetting the deduction guide in C++17, which results in a confusing "class template argument deduction failed" error. In C++20 and later the guide is redundant.

std::monostate for default-constructible variants

std::monostate solves the problem of a variant whose first alternative is not default-constructible. Without it, std::variant<NonDefaultConstructible, ...> cannot be default-constructed, because the standard default-constructs alternative 0.

cpp
// C++17
#include <variant>

struct Socket {
    explicit Socket(int fd);  // no default constructor
};

struct FileHandle {
    explicit FileHandle(const char* path);  // no default constructor
};

// Without monostate: variant<Socket, FileHandle> v;  β†’ compile error

// With monostate: the variant starts in a well-defined "nothing" state
std::variant<std::monostate, Socket, FileHandle> resource;
// resource.index() == 0; holds monostate

resource.emplace<Socket>(3);         // activate Socket
resource.emplace<FileHandle>("/tmp/f");  // activate FileHandle
resource.emplace<std::monostate>();  // reset back to empty

if (std::holds_alternative<std::monostate>(resource)) {
    // safely do nothing β€” resource is not yet initialized
}

std::monostate is a trivially copyable empty struct, so it never participates in the valueless_by_exception hazard. It is also useful as a sentinel value in switch-style visitation.

cpp
// C++17: treating monostate as "unset" in a visitor
std::visit(overloaded{
    [](std::monostate)          { /* not yet initialized */ },
    [](const Socket& s)         { /* use socket */ },
    [](const FileHandle& fh)    { /* use file */ },
}, resource);

valueless_by_exception: when it happens

A variant becomes valueless_by_exception only when a move-construction throws during an assignment after the previous value has already been destroyed. This is the one situation where std::variant cannot maintain its type-safety invariant without double-buffering.

The sequence of events during v = T{args...} (simplified):

  1. Construct a temporary T on the stack.
  2. Destroy the current alternative inside v.
  3. Move-construct T into v's buffer.

If step 3 throws, the old value is gone and the new one never landed β€” the variant is stranded. v.valueless_by_exception() returns true, v.index() returns std::variant_npos, and both std::get and std::visit throw std::bad_variant_access.

cpp
// C++17: deliberately triggering valueless_by_exception
struct Volatile {
    Volatile() = default;
    Volatile(const Volatile&) = default;
    Volatile(Volatile&&) noexcept(false) { throw std::runtime_error{"oops"}; }
};

std::variant<int, Volatile> v = 0;  // holds int
try {
    v = Volatile{};   // destroys int, then move throws
} catch (...) {}

assert(v.valueless_by_exception());  // true
// v.index() == std::variant_npos
// std::visit(..., v) would throw std::bad_variant_access

The complete prevention strategy: mark all move constructors on variant alternatives noexcept. The standard guarantees that if every T_i is nothrow-move-constructible, std::variant assignment never enters the valueless state.

cpp
// C++17: safe design
struct Safe {
    Safe() = default;
    Safe(Safe&&) noexcept = default;  // noexcept move β†’ valueless impossible
};

Recursive variant types

A std::variant cannot directly contain itself because the type would be incomplete at the point of instantiation. Break the cycle with a pointer indirection; std::unique_ptr is idiomatic because it models single ownership.

cpp
// C++17: JSON-like recursive value type
#include <variant>
#include <memory>
#include <string>
#include <vector>
#include <map>

struct JsonNull {};
struct JsonBool  { bool value; };
struct JsonNum   { double value; };
struct JsonStr   { std::string value; };
struct JsonArray;
struct JsonObject;

using JsonValue = std::variant<
    JsonNull,
    JsonBool,
    JsonNum,
    JsonStr,
    std::unique_ptr<JsonArray>,
    std::unique_ptr<JsonObject>
>;

struct JsonArray  { std::vector<JsonValue> items; };
struct JsonObject { std::map<std::string, JsonValue> fields; };

Ownership is clear: JsonArray and JsonObject own their children through unique_ptr. Copying requires a deep-clone helper because unique_ptr is move-only; serialization and visitation work naturally through std::visit.

cpp
// C++17: visiting a recursive JSON tree
void print_json(const JsonValue& v, int depth = 0) {
    std::string indent(depth * 2, ' ');
    std::visit(overloaded{
        [&](JsonNull)               { printf("%snull\n", indent.c_str()); },
        [&](JsonBool b)             { printf("%s%s\n", indent.c_str(), b.value ? "true" : "false"); },
        [&](JsonNum n)              { printf("%s%g\n", indent.c_str(), n.value); },
        [&](const JsonStr& s)       { printf("%s\"%s\"\n", indent.c_str(), s.value.c_str()); },
        [&](const std::unique_ptr<JsonArray>& a) {
            for (const auto& item : a->items) print_json(item, depth + 1);
        },
        [&](const std::unique_ptr<JsonObject>& o) {
            for (const auto& [k, val] : o->fields) {
                printf("%s%s:\n", indent.c_str(), k.c_str());
                print_json(val, depth + 1);
            }
        },
    }, v);
}

Pattern: variant-based state machine

std::variant maps naturally onto a finite-state machine because each alternative IS a state, and transitions are just emplace calls. Illegal states are unrepresentable by construction.

cpp
// C++17: TCP-like connection state machine
#include <variant>
#include <string>

struct Closed   {};
struct Listening{ int port; };
struct Connected{ std::string peer_addr; int fd; };
struct Closing  { int fd; };

using TcpState = std::variant<Closed, Listening, Connected, Closing>;

struct Connection {
    TcpState state;

    void listen(int port) {
        // Only valid from Closed
        assert(std::holds_alternative<Closed>(state));
        state.emplace<Listening>(port);
    }

    void accept(std::string peer, int fd) {
        // Only valid from Listening
        assert(std::holds_alternative<Listening>(state));
        state.emplace<Connected>(std::move(peer), fd);
    }

    void begin_close() {
        auto* c = std::get_if<Connected>(&state);
        assert(c != nullptr);
        int fd = c->fd;
        state.emplace<Closing>(fd);
    }

    void on_event() {
        std::visit(overloaded{
            [](Closed&)              { /* nothing to do */ },
            [](Listening& l)         { /* accept new connection on l.port */ },
            [](Connected& c)         { /* read/write c.fd */ },
            [](Closing& cl)          { /* drain and close cl.fd */ },
        }, state);
    }
};

The key benefit over an enum + raw data struct: the compiler enforces that peer_addr and fd are only accessible in Connected, not in every state. Transitions that store state-specific data are self-documenting.

variant vs union vs std::any

Choose the abstraction that matches the constraints of your use case. The three mechanisms exist on a spectrum from maximum performance / minimum safety (union) to maximum flexibility / minimum compile-time guarantees (std::any).

Featureunionstd::variantstd::any
Type setFixed, manual tagFixed, automatic tagOpen, unbounded
Heap allocationNeverNeverSometimes (SBO)
Default-constructibleDepends on membersIf first alt is DC or monostateYes
Copy/moveManualAutomaticAutomatic
Type accessUB on wrong memberbad_variant_accessbad_any_cast
Exhaustive visitNoYes, via std::visitNo
constexpr supportYesC++20No
Runtime type overhead0 (no tag)1 discriminant byte–wordtype_info*
cpp
// C++17: same data, three representations

// union β€” no tag, programmer tracks which member is live
union RawNum { int i; double d; };
RawNum u; u.d = 3.14;  // must remember it holds double

// variant β€” tag built in, access is checked
std::variant<int, double> v = 3.14;
double d = std::get<double>(v);  // safe; throws if wrong

// any β€” type erased; type checked only at cast time
std::any a = 3.14;
double e = std::any_cast<double>(a);  // safe; throws if wrong type
// a = std::string{"hello"};  // OK: any can hold anything

Use union when you must control layout for ABI, SIMD, or memory-mapped I/O. Use std::variant for any application-level sum type with a known fixed set of alternatives. Use std::any when the set of types is open or determined at runtime (e.g., plugin values, scripting bindings).

See Also

  • std::optional β€” single-type optional value (C++17)
  • std::expected<T, E> β€” binary result/error type with monadic operations (C++23)
  • std::any β€” type-erased container for an open, unbounded set of types (C++17)
  • std::monostate β€” trivial empty sentinel type for use as a variant alternative (C++17)
  • std::visit β€” primary visitation interface; overloads for multi-variant dispatch (C++17, constexpr C++20, explicit R C++23)