std::variant (C++17, <variant>) is a type-safe union: a value that, at any moment, holds exactly one of a fixed set of alternative types. Unlike the C union, the variant knows which alternative is active and a wrong get is detected at compile time or throws.
std::visit is the partner function that dispatches to the right overload for whichever alternative is currently active. Together they replace most uses of inheritance-based "discriminated union" hierarchies.
- 1. Basic usage
- 2. Querying the active alternative
- 3.
std::visit— the right way to consume a variant - 4. Real-world examples
- 5.
variant,optional,expected— picking the right one - 6. Edge cases and pitfalls
#include <variant>
#include <string>
std::variant<int, std::string> v; // default-initialized — holds int{0}
v = 42; // now holds int
v = std::string{"hello"}; // now holds std::stringAssignment destroys the old alternative and constructs the new one in place. The variant is never empty (except for the rare "valueless by exception" state — see §6).
To construct directly with a specific alternative when types overlap:
std::variant<int, double, int> v{std::in_place_index<2>, 7}; // pick the second int
std::variant<int, std::string> w{std::in_place_type<std::string>, "hi"};std::variant<int, std::string> v = 42;
if (std::holds_alternative<int>(v)) // true / false
std::cout << std::get<int>(v); // throws std::bad_variant_access if wrong
v.index(); // 0 (returns size_t — index into the type list)
if (auto p = std::get_if<int>(&v)) // pointer, nullptr if wrong alternative
std::cout << *p; // no exception pathstd::get_if is the right choice when "wrong alternative" is a normal outcome you want to branch on; std::get is for when you've already verified the alternative.
Manually chaining holds_alternative / get is verbose and easy to get wrong (forget a type, add a new alternative and miss a branch). std::visit solves both: you pass a callable, and it's invoked with whichever alternative is currently active.
std::variant<int, double, std::string> v = 3.14;
std::visit([](auto&& x) {
std::cout << x << '\n';
}, v); // prints 3.14 — operator<< picked for doubleauto&& makes the lambda a template that accepts any alternative. If every alternative supports the operation (here, operator<<), nothing more is needed.
When alternatives need different handling, branch on the deduced type:
std::visit([](auto&& x) {
using T = std::decay_t<decltype(x)>;
if constexpr (std::is_same_v<T, int>)
std::cout << "int: " << x;
else if constexpr (std::is_same_v<T, double>)
std::cout << "double: " << x;
else if constexpr (std::is_same_v<T, std::string>)
std::cout << "string: " << x;
}, v);if constexpr means only the matching branch is instantiated — branches that wouldn't compile for the other alternatives (e.g. x.size() on an int) are fine.
A small overloaded helper turns several lambdas into one callable, and std::visit picks the matching overload:
template <class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
template <class... Ts> overloaded(Ts...) -> overloaded<Ts...>; // CTAD, C++17
std::visit(overloaded{
[](int x) { std::cout << "int " << x; },
[](double x) { std::cout << "double " << x; },
[](const std::string& s) { std::cout << "string " << s; },
}, v);The compiler errors if any alternative is unhandled — your switch is exhaustive by construction. Adding a new alternative to the variant and forgetting to handle it becomes a compile error rather than a runtime surprise.
std::visit forwards the result. The common return type must exist (or be explicitly specified in C++20 via std::visit<R>):
double as_double = std::visit(overloaded{
[](int x) { return double(x); },
[](double x) { return x; },
[](const std::string& s) { return std::stod(s); },
}, v);You can pass several variants; the callable is invoked with one argument per variant:
std::variant<int, double> a = 1;
std::variant<int, double> b = 2.0;
std::visit([](auto x, auto y) {
std::cout << x + y; // works for any (int, int), (int, double), …
}, a, b);using ConfigValue = std::variant<int, double, std::string, bool>;
std::unordered_map<std::string, ConfigValue> config = {
{"max_connections", 100},
{"timeout", 30.5},
{"username", std::string{"admin"}},
{"debug", true},
};
for (auto& [k, v] : config) {
std::cout << k << ": ";
std::visit([](auto&& x) { std::cout << x << '\n'; }, v);
}struct Circle { double radius; };
struct Rectangle { double w, h; };
struct Triangle { double base, height; };
using Shape = std::variant<Circle, Rectangle, Triangle>;
double area(const Shape& s) {
return std::visit(overloaded{
[](const Circle& c) { return 3.14159 * c.radius * c.radius; },
[](const Rectangle& r) { return r.w * r.h; },
[](const Triangle& t) { return 0.5 * t.base * t.height; },
}, s);
}No virtual dispatch, no heap allocation, exhaustive at compile time, and the shape types stay simple value types. This is the "sum type" pattern from functional languages — apt whenever the set of alternatives is closed and known up front.
A single channel that can carry IMU, GPS, or LiDAR samples, dispatched in one place:
struct Imu { double ax, ay, az, gx, gy, gz; };
struct Gps { double lat, lon, alt; };
struct Scan { std::vector<float> ranges; };
using SensorMsg = std::variant<Imu, Gps, Scan>;
void handle(const SensorMsg& m) {
std::visit(overloaded{
[](const Imu& i) { ekf.predict(i); },
[](const Gps& g) { ekf.correct_position(g); },
[](const Scan& s) { mapping.integrate(s); },
}, m);
}Adding a fourth sensor (Barometer) to the variant produces compile errors at every visit call that hasn't been updated — exactly the safety property you want when the message set changes.
| Type | Holds | Use when |
|---|---|---|
std::optional<T> |
T or nothing |
A value may be absent and absence carries no extra information. Cache lookups, find_*. |
std::variant<A, B, …> |
One of N concrete types | The set of alternatives is closed and known at compile time. Messages, shapes, parser tokens. |
std::expected<T, E> (C++23) |
T or E |
Operation can succeed or fail, and failure carries a typed reason. Parsing, IO, validation. |
std::any |
Anything | Last resort — type-erased single value when alternatives aren't known. Heavier and visit-by-type-id only. |
A few decision rules:
Lookup where "not found" is normal → std::optional<T>
std::optional<User> find_user(UserId id) const;Absence is a valid outcome, not an error worth explaining. Use std::optional<std::reference_wrapper<const User>> to avoid the copy.
Operation can fail with a reason → std::expected<T, E>
enum class ParseErr { Empty, InvalidNumber, OutOfRange };
std::expected<int, ParseErr> parse_port(std::string_view s);Callers want to recover or report differently per error kind.
A fixed set of alternative payloads → std::variant
using Token = std::variant<Number, Identifier, Operator, EndOfFile>;Every alternative is "normal" — none is an error.
Avoid try-style out-parameters (bool try_parse(string_view, int&)). Use optional or expected instead — they're explicit at the call site and compose.
Bridging the three:
std::expected<User, Err> require_user(UserId id) {
if (auto u = find_user(id)) return *u; // optional → expected
return std::unexpected(Err::NotFound);
}valueless_by_exception()— if an assignment throws partway through, the variant can end up in a special "no alternative" state.index() == variant_npos, andvisitthrowsstd::bad_variant_access. Rare in practice (only when the new alternative's move/copy throws), but it's the answer to "why doesvisithave a throwing path even though I always set a value." Use types withnoexceptmove to avoid this entirely.- Reference and
voidalternatives are not allowed. Usestd::reference_wrapper<T>if you need a reference alternative. - Default construction picks the first alternative. If that's not what you want, list a
std::monostatefirst to make the variant default-constructible without committing to a real type. std::get<T>is O(N) at compile time, O(1) at runtime.std::visitgenerates a jump table indexed byindex()— same cost as aswitch.- Don't store
std::variant<Base*, Derived*>. That's not what variant is for; use a single base pointer with virtual dispatch, or a variant of value types.