std::variant
What problem does this section solve?
Sometimes a variable needs to store a value that could be an int, a string, or a double. The traditional approach is union (in C), but it's not type-safe — you don't know which type is currently stored, and if you access it incorrectly, it crashes.
std::variant is a type-safe union that can store one of multiple types and knows which type it currently holds.
What is this feature?
std::variant<T1, T2, ...> is a type-safe union introduced in C++17. At any given time, it stores a value of only one of its contained types. When you access it, the compiler performs checks to ensure you don't accidentally retrieve a value of the wrong type.
C++ standard version
C++17
Required header files
#include <variant>
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
Common Usage
| Operation | Explanation |
|---|---|
v = value; | Assignment (Automatic Type Switching) |
v.emplace<T>(args...) | In-place construction |
std::get<T>(v) | Get value (throws std::bad_variant_access if the type is incorrect) |
std::get_if<T>(&v) | Safe retrieval (returns nullptr if type does not match) |
v.index() | Return the current type index (0-based) |
std::visit(visitor, v) | Use the visitor pattern to handle |
std::holds_alternative<T>(v) | Check if holding type T |
Example code
Example 1: variant basic usage—storing different types of values
#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;
}
Results:
int: 42
double: 3.14
string: hello
current index: 2
Example 2: Building on Example 1, using 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;
}
Results:
int: 42
double: 3.14159
string: C++17
Example 3:Building 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;
}
Results:
value: 42
value: 2.718
value: hello variant
to_double: 10
Example 4: Building on Example 3, using variant to represent message types
#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;
}
Results:
Text: Hello World
Number: 42
Quit!
std::visit 与 Visitor 机制
上面的代码中:
std::visit(MessageHandler{}, msg);
这一句刚开始看起来可能比较奇怪。
先记住 std::visit 最基本的形式:
std::visit(visitor, variant对象);
Among them:
- 第二个参数是
std::variant
- 第一个参数是一个可调用对象(Callable)
std::visit会根据variant当前保存的数据类型,调用对应的处理函数
For example:
Message msg;
msg = TextMessage{"Hello World"};
std::visit(MessageHandler{}, msg);
此时 msg 内部保存的是:
TextMessage
因此 std::visit 会把这个 TextMessage 取出来,并交给 MessageHandler 处理。
MessageHandler{} 是什么?
这里:
MessageHandler{}
不是函数,也不是特殊语法。
它就是创建了一个临时的 MessageHandler 对象。
For example:
MessageHandler handler;
和:
MessageHandler{}
创建的对象类型是一样的,只不过后者是临时对象。
因此:
std::visit(MessageHandler{}, msg);
It can also be written as:
MessageHandler handler;
std::visit(handler, msg);
为什么一个结构体可以像函数一样调用?
因为 MessageHandler 重载了:
operator()
For example:
struct MessageHandler
{
void operator()(const TextMessage& msg) const
{
std::cout << "Text: " << msg.text << "\n";
}
};
创建对象以后:
MessageHandler handler;
就可以直接这样调用:
handler(TextMessage{"Hello"});
看起来像是在调用一个函数。
实际上等价于:
handler.operator()(TextMessage{"Hello"});
所以这种重载了:
operator()
的对象也叫:
函数对象(Function Object / Functor)。
为什么这里有三个 operator()?
因为:
using Message =
std::variant<TextMessage, NumberMessage, QuitMessage>;
Message 可能保存三种不同的数据:
TextMessage
NumberMessage
QuitMessage
因此 MessageHandler 分别准备了三个处理函数:
void operator()(const TextMessage& msg) const
{
std::cout << "Text: " << msg.text << "\n";
}
处理:
TextMessage
void operator()(const NumberMessage& msg) const
{
std::cout << "Number: " << msg.number << "\n";
}
处理:
NumberMessage
void operator()(const QuitMessage&) const
{
std::cout << "Quit!\n";
}
处理:
QuitMessage
它们虽然函数名都是:
operator()
但是参数类型不同,因此属于函数重载。
std::visit 到底做了什么?
For example:
msg = TextMessage{"Hello World"};
std::visit(MessageHandler{}, msg);
此时 msg 保存的是:
TextMessage
可以粗略理解为 std::visit 做了:
MessageHandler{}(TextMessage{"Hello World"});
于是编译器找到:
void operator()(const TextMessage& msg) const
最终输出:
Text: Hello World
Another example:
msg = NumberMessage{42};
std::visit(MessageHandler{}, msg);
可以粗略理解成:
MessageHandler{}(NumberMessage{42});
于是调用:
void operator()(const NumberMessage& msg) const
输出:
Number: 42
最后:
msg = QuitMessage{};
std::visit(MessageHandler{}, msg);
则会选择:
void operator()(const QuitMessage&) const
输出:
Quit!
整个过程可以理解为:
std::variant
│
┌───────────────┼───────────────┐
│ │ │
TextMessage NumberMessage QuitMessage
│ │ │
▼ ▼ ▼
operator(TextMessage) operator(NumberMessage) operator(QuitMessage)
std::visit 的作用就是:
查看
variant当前保存的是哪一种类型,然后自动调用 visitor 中能够处理这个类型的函数。
std::visit 第一个参数必须是结构体吗?
不是。
std::visit 第一个参数本质上只要求是一个:
可调用对象(Callable)。
也就是说,只要一个东西能够像下面这样调用:
对象(参数);
就可以作为 visitor。
常见的可调用对象包括:
普通函数
Lambda
函数对象(重载 operator() 的 class / struct)
std::function
上面的例子使用的是:
MessageHandler{}
它属于:
结构体对象
↓
重载 operator()
↓
函数对象 Functor
↓
可以作为 std::visit 的 Visitor
Lambda 为什么也能作为 Visitor?
For example:
std::visit(
[](const auto& msg)
{
std::cout << "收到一条消息\n";
},
msg
);
这里第一个参数就是一个 Lambda。
Lambda 本身也是一种可调用对象。
For example:
auto f = [](int x)
{
std::cout << x << "\n";
};
f(10);
能够像函数一样调用:
f(10);
因此它也可以传给:
std::visit
实际上,可以把 Lambda 粗略理解成编译器自动生成了一个匿名的函数对象:
struct 某个匿名类型
{
void operator()(int x) const
{
std::cout << x << "\n";
}
};
所以从思想上来说:
Lambda
和:
重载了 operator() 的 struct/class
非常相似。
为什么这个例子更适合用结构体 Visitor?
如果只有一种简单操作,Lambda 很方便:
[](const auto& value)
{
// ...
}
但是这里需要针对不同类型执行完全不同的逻辑:
TextMessage
NumberMessage
QuitMessage
使用多个 operator() 重载会非常直观:
struct MessageHandler
{
void operator()(const TextMessage& msg) const
{
// 处理文本消息
}
void operator()(const NumberMessage& msg) const
{
// 处理数字消息
}
void operator()(const QuitMessage&) const
{
// 处理退出消息
}
};
这样每种消息类型都有自己独立的处理逻辑。
核心理解
可以把:
std::visit(MessageHandler{}, msg);
拆成两个部分理解。
首先:
MessageHandler{}
Indicates:
创建一个 MessageHandler 临时对象
由于它重载了:
operator()
所以它是一个可调用对象。
然后:
std::visit(..., msg);
负责:
查看 msg 当前保存的类型
↓
取出对应的数据
↓
调用 MessageHandler 对应的 operator()
因此:
std::visit(MessageHandler{}, msg);
可以概括成一句话:
根据
msg当前保存的数据类型,让MessageHandler自动选择对应的operator()进行处理。
也可以记成:
std::visit(visitor, variant);
即:
visit
│
├── 看 variant 当前是什么类型
│
├── 把里面的数据取出来
│
└── 用这个数据调用 visitor
其中 visitor 并不是某一种固定语法,而是任何能够被调用的对象。
在本例中:
MessageHandler{}
就是一个通过重载 operator() 实现的函数对象。
runtime results
See the "running results" for each example above.
Key syntax explanation in the example
|Here is the translation of the provided Simplified Chinese Markdown fragment into natural American English, following all specified rules.
| Example | Discusses what | Newly emerged syntax | Why write it this way | Precautions |
|---|---|---|---|---|
| Example 1 | Basic Assignment and get | std::variant<int, double, string>、std::get<T>(v) | A variant is type-safe and automatically switches its type when assigned. | An incorrect type will throw an exception. |
| Example 2 | get_if safe access | std::get_if<T>(&v) | returns pointer, if type mismatch returns nullptr | More secure than get, recommended |
| Example 3 | visitor pattern | std::visit(lambda, v) | visit forces coverage of all types, making it the optimal way to access a variant. | Generic lambdas + std::visit is the most concise combination. |
| Example 4 | Message Distribution Pattern | struct visitor + variant | Using variant and visitor for type-safe message handling | Visitors must provide an operator() for each type. |
Variant is suitable for "one of a limited number of types."
variant isn't meant to replace all inheritance and polymorphism. It's best suited for scenarios where you have a limited set of type kinds and you want the compiler to remind you to handle all the cases.
| Scene | Recommendation |
|---|---|
| Messages are only of three types: Text, Number, and Quit. | std::variant |
| States only consist of a few categories: Idle / Running / Error. | std::variant |
| The parsing result could be of type int, double, or string. | std::variant |
| There are many types and they require runtime plugin extensions. | Inheritance + Virtual Functions |
| All objects share a common interface. | Polymorphic interfaces are more natural. |
Example 5: Using variant to represent a state machine
#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;
}
Results:
state: idle
state: running task 42
state: error motor timeout
Here, the state can only ever be one of three. Rather than adding extra fields with int state_code, variant can place the data needed for each state within its corresponding type, reducing issues like "reading the running field while in an error state."
Common Errors
Error 1: Incorrect type used with get, causing exceptions to be thrown.
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 variant has no default type
std::variant<int, std::string> v; // 默认构造第一个类型的默认值(int = 0)
This situation is valid, but if the first type lacks a default constructor, compilation fails.
Error 3: Visitor of visit has not covered 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 for visit must provide operator() for all types in the variant, or use a generic lambda.
使用建议
- 明确目标:在开始前确定您的具体需求,以便选择最合适的工具或教程。
- 充分利用资源:参考官方文档、教程和博客,这些资料能帮助您快速上手并解决问题。
- 实践应用:通过动手操作项目或编写代码来巩固学习成果,提升实际操作能力。
- 问题解决:遇到困难时,查阅参考资料或寻求社区支持,逐步培养独立解决问题的能力。
- 分享经验:完成项目后,可以撰写文章或博客分享心得,帮助其他学习者。
如果需要针对特定领域(如单片机、机器人或环境搭建)的进一步建议,请提供更多信息,我将为您细化内容。
- 替代
union:type-safe variant, knows what it currently holds. - Using
std::visit+ generic lambda is the most concise way to access. - When you need to "know the current type": return a pointer, safely and efficiently.
- Implementing message/event dispatching with variant + visit: A rudimentary form of pattern matching.
- The size of a variant is the largest among all types + the index field: Avoid storing many large types.
- variant is clearer when type kinds are limited: If types need to be extended with plugins, inheritance and virtual functions are usually more appropriate.
Summary
std::variant<T1, T2, ...>is a type-safe union.std::get<T>(v)direct access (unsafe),std::get_if<T>(&v)returns pointer (safe).std::visit(visitor, v)is the most recommended approach to force coverage for all types.- Suitable for scenarios like message distribution, optional configuration, and state machines.