Wiki
std::variant
Existing English translations were reused. 3 current source blocks remain in Simplified Chinese, without triggering paid translation.
1.What problem does this section solve?
Sometimes a variable needs to store a value that "might be an int, might be a string, or might be a double." The traditional approach is union (C language), but it is not type-safe—you do not know which type is currently stored, and accessing it incorrectly will cause a crash.
std::variant is a type-safe union that can store one of multiple types andknows which type is currently stored.
2.What is this feature?
std::variant<T1, T2, ...> is a type-safe union introduced in C++17. At any given time, it stores the value of only one of its types. When accessed, the compiler will check for you, so the problem of "accessing the wrong type" does not occur.
3.C++ Standard Edition
C++17
4.Required header files
#include <variant>
5.Basic syntax
std::variant<int, double, std::string> v;
v = 42; // 存 int
v = 3.14; // 存 double
v = std::string("hello"); // 存 string
// 访问方式 1:std::get<T>(v) —— 类型不对抛异常
int n = std::get<int>(v);
// 访问方式 2:std::get_if<T>(&v) —— 类型不对返回 nullptr
if (auto* p = std::get_if<int>(&v)) { ... }
// 访问方式 3:std::visit —— 用 visitor 模式处理所有可能的类型
std::visit([](auto&& val) { ... }, v);
// 查询当前存储的类型的索引
size_t idx = v.index(); // 0-based
6.Common Usage
| Operation | Description |
|---|---|
v = value; |
Assignment (automatic type switching) |
v.emplace<T>(args...) |
In-place construction |
std::get<T>(v) |
Get value (throws if the type is incorrect: std::bad_variant_access) |
std::get_if<T>(&v) |
Safe get (returns nullptr if the type is incorrect) |
v.index() |
Return the index of the current type (0-based) |
std::visit(visitor, v) |
Process using the visitor pattern |
std::holds_alternative<T>(v) |
Determine whether it holds type T |
7.Example code
7.1.Example 1: Basic usage of variant—storing values of different types
#include <iostream>
#include <variant>
#include <string>
#include <type_traits>
int main()
{
// v 可以存 int、double 或 string
std::variant<int, double, std::string> v;
v = 42;
std::cout << "int: " << std::get<int>(v) << "\n";
v = 3.14;
std::cout << "double: " << std::get<double>(v) << "\n";
v = std::string("hello");
std::cout << "string: " << std::get<std::string>(v) << "\n";
// 查看当前类型索引
std::cout << "current index: " << v.index() << "\n"; // 2 (string)
return 0;
}
Running result:
int: 42
double: 3.14
string: hello
current index: 2
7.2.Example 2: Based on Example 1, use get_if for safe access
#include <iostream>
#include <variant>
#include <string>
void print_value(const std::variant<int, double, std::string>& v)
{
// 安全方式:逐个尝试,get_if 返回指针
if (auto* p = std::get_if<int>(&v))
{
std::cout << "int: " << *p << "\n";
}
else if (auto* p = std::get_if<double>(&v))
{
std::cout << "double: " << *p << "\n";
}
else if (auto* p = std::get_if<std::string>(&v))
{
std::cout << "string: " << *p << "\n";
}
}
int main()
{
std::variant<int, double, std::string> v;
v = 42;
print_value(v);
v = 3.14159;
print_value(v);
v = std::string("C++17");
print_value(v);
return 0;
}
Running result:
int: 42
double: 3.14159
string: C++17
7.3.Example 3: Based on Example 2, use std::visit to handle all types
#include <iostream>
#include <variant>
#include <string>
int main()
{
std::variant<int, double, std::string> v;
// std::visit 配合泛型 lambda 优雅处理所有类型
auto printer = [](const auto& val) {
std::cout << "value: " << val << "\n";
};
v = 42;
std::visit(printer, v);
v = 2.718;
std::visit(printer, v);
v = std::string("hello variant");
std::visit(printer, v);
// 也可以返回不同类型的值
auto to_double = [](const auto& val) -> double {
if constexpr (std::is_same_v<std::decay_t<decltype(val)>, std::string>)
{
return 0.0; // string 不能转 double
}
else
{
return static_cast<double>(val);
}
};
v = 10;
std::cout << "to_double: " << std::visit(to_double, v) << "\n";
return 0;
}
Running result:
value: 42
value: 2.718
value: hello variant
to_double: 10
7.4.Example 4: Based on Example 3, use variant to represent the message type
#include <iostream>
#include <variant>
#include <string>
// 定义消息类型
struct TextMessage { std::string text; };
struct NumberMessage { int number; };
struct QuitMessage {};
using Message = std::variant<TextMessage, NumberMessage, QuitMessage>;
// 处理消息的 visitor
struct MessageHandler
{
void operator()(const TextMessage& msg) const
{
std::cout << "Text: " << msg.text << "\n";
}
void operator()(const NumberMessage& msg) const
{
std::cout << "Number: " << msg.number << "\n";
}
void operator()(const QuitMessage&) const
{
std::cout << "Quit!\n";
}
};
int main()
{
Message msg;
msg = TextMessage{"Hello World"};
std::visit(MessageHandler{}, msg);
msg = NumberMessage{42};
std::visit(MessageHandler{}, msg);
msg = QuitMessage{};
std::visit(MessageHandler{}, msg);
return 0;
}
Running result:
Text: Hello World
Number: 42
Quit!
7.5.std::visit and the Visitor mechanism
In the above code:
std::visit(MessageHandler{}, msg);
This sentence may look rather strange at first.
First remember std::visit the most basic form:
std::visit(visitor, variant对象);
Among them:
- The second parameter is
std::variant - The first parameter is acallable object (Callable)
std::visitand will, based on thevariantcurrently stored data type, call the corresponding handler function
For example:
Message msg;
msg = TextMessage{"Hello World"};
std::visit(MessageHandler{}, msg);
At this point, msg what is stored internally is:
TextMessage
Therefore, std::visit will extract this TextMessage and hand it over to MessageHandler for processing.
7.5.1.MessageHandler{}What is it?
Here:
MessageHandler{}
It is neither a function nor special syntax.
It just created a temporary MessageHandler object.
For example:
MessageHandler handler;
and:
MessageHandler{}
The object type created is the same, except that the latter is a temporary object.
Therefore:
std::visit(MessageHandler{}, msg);
It can also be written as:
MessageHandler handler;
std::visit(handler, msg);
7.5.2.Why can a struct be called like a function?
Because MessageHandler is overloaded:
operator()
For example:
struct MessageHandler
{
void operator()(const TextMessage& msg) const
{
std::cout << "Text: " << msg.text << "\n";
}
};
After creating the object:
MessageHandler handler;
You can directly call it like this:
handler(TextMessage{"Hello"});
It looks like it is calling a function.
In fact, this is equivalent to:
handler.operator()(TextMessage{"Hello"});
So this kind of overloaded:
operator()
The objects of also called:
Function Object / Functor.
7.5.3.Why are there three operator() here?
Because:
using Message =
std::variant<TextMessage, NumberMessage, QuitMessage>;
Message may store three different types of data:
TextMessage
NumberMessage
QuitMessage
Therefore, MessageHandler three handler functions are prepared respectively:
void operator()(const TextMessage& msg) const
{
std::cout << "Text: " << msg.text << "\n";
}
Processing:
TextMessage
void operator()(const NumberMessage& msg) const
{
std::cout << "Number: " << msg.number << "\n";
}
Processing:
NumberMessage
void operator()(const QuitMessage&) const
{
std::cout << "Quit!\n";
}
Processing:
QuitMessage
Although their function names are all:
operator()
However, the parameter types are different, so it isfunction overloading.
7.5.4.std::visit What exactly was done?
For example:
msg = TextMessage{"Hello World"};
std::visit(MessageHandler{}, msg);
At this point, msg stores:
TextMessage
It can be roughly understood as std::visit did:
MessageHandler{}(TextMessage{"Hello World"});
Thus, the compiler finds:
void operator()(const TextMessage& msg) const
Final output:
Text: Hello World
For another example:
msg = NumberMessage{42};
std::visit(MessageHandler{}, msg);
It can be roughly understood as:
MessageHandler{}(NumberMessage{42});
Then call:
void operator()(const NumberMessage& msg) const
Output:
Number: 42
Finally:
msg = QuitMessage{};
std::visit(MessageHandler{}, msg);
then it will select:
void operator()(const QuitMessage&) const
Output:
Quit!
The entire process can be understood as:
std::variant
│
┌───────────────┼───────────────┐
│ │ │
TextMessage NumberMessage QuitMessage
│ │ │
▼ ▼ ▼
operator(TextMessage) operator(NumberMessage) operator(QuitMessage)
std::visitThe function of is:
Check
variantwhich type is currently stored, and then automatically call the function in the visitor that can handle this type.
7.5.5.std::visit Must the first parameter be a struct?
No.
std::visitThe first parameter essentially only requires being a:
Callable.
That is, as long as something can be called as follows:
对象(参数);
it can serve as a visitor.
Common callable objects include:
普通函数
Lambda
函数对象(重载 operator() 的 class / struct)
std::function
The above example uses:
MessageHandler{}
It belongs to:
结构体对象
↓
重载 operator()
↓
函数对象 Functor
↓
可以作为 std::visit 的 Visitor
7.5.6.Why can a Lambda also serve as a Visitor?
For example:
std::visit(
[](const auto& msg)
{
std::cout << "收到一条消息\n";
},
msg
);
Here the first parameter is a Lambda.
A lambda itself is also a callable object.
For example:
auto f = [](int x)
{
std::cout << x << "\n";
};
f(10);
Can be called like a function:
f(10);
Therefore, it can also be passed to:
std::visit
In practice, Lambda can be roughly understood as the compiler automatically generating an anonymous function object:
struct 某个匿名类型
{
void operator()(int x) const
{
std::cout << x << "\n";
}
};
So conceptually speaking:
Lambda
and:
重载了 operator() 的 struct/class
Very similar.
7.5.7.Why is this example more suitable for using a struct Visitor?
If there is only one simple operation, Lambda is very convenient:
[](const auto& value)
{
// ...
}
However, here completely different logic needs to be executed for different types:
TextMessage
NumberMessage
QuitMessage
Using multiple operator() overloads can be very intuitive:
struct MessageHandler
{
void operator()(const TextMessage& msg) const
{
// 处理文本消息
}
void operator()(const NumberMessage& msg) const
{
// 处理数字消息
}
void operator()(const QuitMessage&) const
{
// 处理退出消息
}
};
In this way, each message type has its own independent processing logic.
7.5.8.Core Understanding
You can:
std::visit(MessageHandler{}, msg);
Understand it by splitting it into two parts.
First:
MessageHandler{}
Indicates:
创建一个 MessageHandler 临时对象
Because it overloads:
operator()
So it is a callable object.
Then:
std::visit(..., msg);
Responsible for:
查看 msg 当前保存的类型
↓
取出对应的数据
↓
调用 MessageHandler 对应的 operator()
Therefore:
std::visit(MessageHandler{}, msg);
It can be summarized in one sentence:
Based on
msgthe currently saved data type, letMessageHandlerautomatically select the correspondingoperator()for processing.
It can also be written as:
std::visit(visitor, variant);
Namely:
visit
│
├── 看 variant 当前是什么类型
│
├── 把里面的数据取出来
│
└── 用这个数据调用 visitor
Here, visitor is not a fixed syntax, but any object that can be called.
In this example:
MessageHandler{}
就是一个通过重载 operator() 实现的函数对象。
8.Operation result
See the "run results" for each example above.
9.Key syntax explanation in the example
| Example | What was discussed | Newly introduced syntax | Why it is written this way | Notes |
|---|---|---|---|---|
| Example 1 | Basic assignment and get | std::variant<int, double, string>、std::get<T>(v) |
variant is type-safe and automatically switches types on assignment | get<T> an incorrect type will throw an exception |
| Example 2 | get_if safe access | std::get_if<T>(&v) |
Returns a pointer; returns nullptr if the type is incorrect | Safer than get, recommended for use |
| Example 3 | visit pattern | std::visit(lambda, v) |
visit forces coverage of all types and is the best way to access a variant | Generic lambda + visit is the most concise combination |
| Example 4 | Message dispatch pattern | struct visitor + variant | Use variant + visitor to implement type-safe message handling | The visitor must provide operator() for every type |
10.variant suitable for "one of a finite number of types"
variantIt is not meant to replace all inheritance and polymorphism. It is best suited for situations where the number of type variants is limited and you want the compiler to remind you to handle every case.
| Scenario | Recommendation |
|---|---|
| Messages have only three types: Text / Number / Quit | std::variant |
| States have only a few types: Idle / Running / Error | std::variant |
| Parse results may be int / double / string | std::variant |
| There are many type kinds and plugins need to be extended at runtime | Inheritance + virtual functions |
| All objects share one interface | Polymorphic interfaces are more natural |
10.1.Example 5: Representing a state machine with variant
#include <iostream>
#include <string>
#include <type_traits>
#include <variant>
struct Idle {};
struct Running
{
int task_id;
};
struct Error
{
std::string message;
};
// variant 表示一个变量可以在多个候选类型中保存其中一种。
using State = std::variant<Idle, Running, Error>;
void print_state(const State& state)
{
// visit 会根据 variant 当前保存的类型调用对应处理逻辑。
std::visit([](const auto& s) {
using T = std::decay_t<decltype(s)>;
if constexpr (std::is_same_v<T, Idle>)
{
std::cout << "state: idle\n";
}
else if constexpr (std::is_same_v<T, Running>)
{
std::cout << "state: running task " << s.task_id << "\n";
}
else if constexpr (std::is_same_v<T, Error>)
{
std::cout << "state: error " << s.message << "\n";
}
}, state);
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
State state = Idle{};
print_state(state);
state = Running{42};
print_state(state);
state = Error{"motor timeout"};
print_state(state);
return 0;
}
Running result:
state: idle
state: running task 42
state: error motor timeout
The state here can only ever be one of three. Compared to using int state_code plus a bunch of extra fields, variant can place the data required for each state within the corresponding type, reducing issues like "reading the running field even though the state is error."
11.Common Errors
Error 1: get called with the wrong type throws an exception
std::variant<int, double> v = 42;
std::cout << std::get<double>(v); // ❌ 抛出 std::bad_variant_access!
Correct approach: first use std::holds_alternative<double>(v) to check, or use std::get_if.
Error 2: Default construction when there is no default type in the variant
std::variant<int, std::string> v; // 默认构造第一个类型的默认值(int = 0)
这种情况是合法的,但如果第一种类型没有默认构造函数,则编译失败。
Error 3: The visitor of visit does not cover all types
struct Visitor {
void operator()(int) {}
// 缺少 double 和 string 的 operator()
};
std::variant<int, double, std::string> v;
std::visit(Visitor{}, v); // ❌ 编译错误!
Correct approach: the visitor of visit must provide for all types in the variant a operator(), or use a generic lambda.
12.Usage Recommendations
- 替代
union:variant 类型安全,知道当前存的是什么。 - 用
std::visit+ 泛型 lambda 是最简洁的访问方式。 - 需要"知道当前是哪种类型"时用
std::get_if:返回指针,安全高效。 - 用 variant + visit 实现消息/事件分发:模式匹配的雏形。
- variant 的大小是所有类型中最大的 + 索引字段:不要存太多大类型。
- 类型种类有限时用 variant 更清晰:如果类型需要随插件扩展,继承和虚函数通常更合适。
13.Summary
std::variant<T1, T2, ...>is a type-safe union.std::get<T>(v)Direct access (unsafe),std::get_if<T>(&v)return pointer (safe).std::visit(visitor, v)is the most recommended approach, enforcing coverage of all types.- Suitable for scenarios such as message dispatch, optional configuration, and state machines.