The std::variant class template, introduced in C++17, represents a type-safe union. An instance of std::variant holds a value of one of its specified alternative types at any given time, or it can be valueless under exceptional circumstances (via valueless_by_exception). It is a safer and more powerful alternative to traditional unions, supporting types like std::string and complex containers such as std::map.
Helper Types
std::monostate
std::monostate is a helper type that serves as a placeholder. It is especially useful when the first alternative type lacks a default constructor, enabling the variant to be default-constructible.
std::variant<std::monostate, int> v; // valid, index 0
std::cout << v.index(); // outputs: 0
std::bad_variant_access
This exception is thrown when you attempt to retrieve a value from a variant using std::get with a type or index that does not match the currently active alternative, or when visiting a variant that is valueless due to an exception.
#include <variant>
#include <iostream>
int main() {
std::variant<int, float> v = 12;
try {
std::get<float>(v);
} catch (const std::bad_variant_access& e) {
std::cout << e.what() << '\n';
}
}
Output:
bad_variant_access
std::variant_size and std::variant_size_v
These compile-time constants provide the number of alternatives in a possibly cv-qualified variant.
#include <variant>
#include <cstdio>
static_assert(std::variant_size_v<std::variant<>> == 0);
static_assert(std::variant_size_v<std::variant<int>> == 1);
static_assert(std::variant_size_v<std::variant<int, float, double>> == 3);
int main() {
std::puts("All assertions passed.");
}
std::variant_alternative and std::variant_alternative_t
These utilities provide compile-time access to the type of a variant's alternative by index. cv-qualifiers on the variant are propagated to the extracted type.
#include <variant>
#include <type_traits>
using my_variant = std::variant<int, float>;
static_assert(std::is_same_v<int, std::variant_alternative_t<0, my_variant>>);
static_assert(std::is_same_v<float, std::variant_alternative_t<1, my_variant>>);
static_assert(std::is_same_v<const int, std::variant_alternative_t<0, const my_variant>>);
int main() {}
Operations on std::variant
Construction
Specify the list of alternative types:
std::variant<uint32_t, double, std::string, int32_t> v;
Initializing with a value selects the best-matching type:
std::variant<uint32_t, double, std::string, int32_t> v{25};
std::cout << v.index(); // outputs: 3 (int32_t)
For multi-argument constructors or to disambiguate, use std::in_place_type or std::in_place_index:
std::variant<std::complex<double>> c{std::in_place_type<std::complex<double>>, 1.0, 2.0};
std::variant<int, int> v{std::in_place_index<1>, 42}; // second int
Important constraints:
std::variantcannot hold references, arrays, orvoid.- An empty variant (with no types) is ill-formed.
Accessing Values
Using std::get (may throw):
std::variant<uint32_t, double, std::string> v = 3.14;
try {
double d = std::get<double>(v);
std::string s = std::get<std::string>(v); // throws
} catch (const std::bad_variant_access& e) {
// handle
}
Using std::get_if (returns pointer or null):
std::variant<int, float> v = 12;
if (auto* p = std::get_if<int>(&v)) {
std::cout << *p; // 12
}
Using std::visit (preferred for efficiency):
Define a visitor with overloaded operator():
struct Visitor {
void operator()(int i) const { std::cout << "int: " << i << '\n'; }
void operator()(float f) const { std::cout << "float: " << f << '\n'; }
void operator()(const std::string& s) const { std::cout << "str: " << s << '\n'; }
};
std::variant<int, float, std::string> v = 3.14f;
std::visit(Visitor{}, v);
Or use a polymorphic lambda with if constexpr:
std::visit([](auto&& arg) {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << arg << '\n';
} else if constexpr (std::is_same_v<T, float>) {
std::cout << "float: " << arg << '\n';
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "str: " << arg << '\n';
}
}, v);
The if constexpr approach evaluates branches at compile time, making it more efficinet than runtime type checking. std::visit also acccepts multiple variant arguments.
Modifying Values
Via std::get (assigns to the active alternative):
std::variant<uint32_t, double, std::string> v;
std::get<double>(v) = 2.71; // activates double alternative
std::get<0>(v) = 100; // now activates uint32_t
std::get<std::string>(v) = "text"; // now activates string
Via std::get_if:
std::variant<int, double> v;
if (auto* p = std::get_if<double>(&v)) {
*p = 3.14;
}
Summary
std::variant is a robust type-safe union that improves upon traditional unions by supporting complex types and providing compile-time checks. For accessing and mdoifying values, prefer std::visit combined with generic lambdas or overloaded visitors to achieve compile-time dispatch and cleaner code.