在C++17之前,我们常用std::any或自定义的union来实现多态容器,但它们往往缺乏类型安全,或者使用起来繁琐。C++17 引入的 std::variant 正是为了填补这一空缺。本文将从语义、实现细节以及常见用法三方面,系统阐述 std::variant 的功能与优势。
一、概念回顾
std::variant<T...> 可以认为是一个“可变形的”值,它在任何给定时间都只持有 T 族中某一个类型的实例。与传统的多态(如继承 + 虚函数)相比,variant 的优势在于:
- 类型安全:编译时就能保证访问到的类型与实际持有的类型一致,避免运行时的
dynamic_cast错误。 - 无运行时开销:不需要虚表,内部实现通常采用联合(
union)和位域来标记当前类型。 - 简洁的语法:
std::visit与std::get等函数极大简化了对多态数据的操作。
二、核心API
| 函数 | 作用 |
|---|---|
std::variant<T...> var; |
默认构造,值为第一个类型的默认值。 |
std::variant<T...> var(v); |
通过传入任意类型 v 进行初始化。 |
| `std::get | |
(var)| 通过索引获取当前值(若类型不匹配抛std::bad_variant_access`)。 |
|
| `std::get | |
(var)| 通过类型获取当前值(若类型不匹配抛std::bad_variant_access`)。 |
|
| `std::holds_alternative | |
(var)| 判断var是否持有类型T`。 |
|
std::visit(f, var) |
访问器函数,f 可是 lambda、函数对象或 std::variant 本身的成员函数。 |
var.swap(other) |
与 other 交换内容。 |
| `var.emplace | |
(args…)| 直接构造T并放入var`。 |
三、实现细节
-
内部存储
variant通常使用一个联合 (union) 存储所有可能类型的内存空间,配合一个整数位域index_记录当前类型的下标。union Storage { T1 t1; T2 t2; ... }; std::size_t index_;通过
index_判断哪一段内存被激活,从而实现“活跃”对象的构造和析构。 -
构造与析构
- 默认构造:直接构造第一个类型
T0。 - 拷贝/移动:复制或移动当前活跃对象,然后更新
index_。 - 析构:根据
index_调用对应类型的析构函数。
- 默认构造:直接构造第一个类型
-
异常安全
` 或 `operator=` 过程中,若构造抛异常,需要确保 `variant` 能恢复到可用状态。实现上通常采用 `try-catch` 包围构造,然后在异常抛出前析构旧值。
在 `emplace
四、常见使用场景
| 场景 | 解决方案 |
|---|---|
| 表示多种状态 | std::variant<State1, State2, State3> 用于实现状态机,避免使用枚举+结构体。 |
| 解析 JSON | JSON 的值可以是字符串、数值、布尔、数组、对象、null;可用 variant<string, double, bool, vector<variant<...>>, map<string, variant<...>>, nullptr_t>。 |
| 函数返回多种结果 | 例如解析函数返回 variant<ParsedValue, ParseError>。 |
| 事件系统 | 不同事件类型通过 variant<EventA, EventB, EventC> 来统一处理。 |
五、示例代码
5.1 简单状态机
#include <variant>
#include <string>
#include <iostream>
struct Idle {};
struct Running { int progress; };
struct Error { std::string msg; };
using State = std::variant<Idle, Running, Error>;
int main() {
State s = Idle{};
std::visit([](auto&& state){
using T = std::decay_t<decltype(state)>;
if constexpr (std::is_same_v<T, Idle>)
std::cout << "Idle\n";
else if constexpr (std::is_same_v<T, Running>)
std::cout << "Running: " << state.progress << "\n";
else if constexpr (std::is_same_v<T, Error>)
std::cout << "Error: " << state.msg << "\n";
}, s);
s = Running{42};
std::visit([](auto&& state){
using T = std::decay_t<decltype(state)>;
if constexpr (std::is_same_v<T, Running>)
std::cout << "Running: " << state.progress << "\n";
}, s);
s = Error{"File not found"};
std::visit([](auto&& state){
using T = std::decay_t<decltype(state)>;
if constexpr (std::is_same_v<T, Error>)
std::cout << "Error: " << state.msg << "\n";
}, s);
}
5.2 解析 JSON 值的基本结构
using JsonValue = std::variant<
std::nullptr_t,
bool,
double,
std::string,
std::vector <JsonValue>,
std::unordered_map<std::string, JsonValue>
>;
void printJson(const JsonValue& v, int indent = 0) {
std::string pad(indent, ' ');
std::visit([&](auto&& val){
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, std::nullptr_t>)
std::cout << "null";
else if constexpr (std::is_same_v<T, bool>)
std::cout << (val ? "true" : "false");
else if constexpr (std::is_same_v<T, double>)
std::cout << val;
else if constexpr (std::is_same_v<T, std::string>)
std::cout << '"' << val << '"';
else if constexpr (std::is_same_v<T, std::vector<JsonValue>>) {
std::cout << "[\n";
for (const auto& e : val) {
std::cout << pad << " ";
printJson(e, indent + 2);
std::cout << ",\n";
}
std::cout << pad << "]";
} else if constexpr (std::is_same_v<T, std::unordered_map<std::string, JsonValue>>) {
std::cout << "{\n";
for (const auto& [k, v] : val) {
std::cout << pad << " \"" << k << "\": ";
printJson(v, indent + 2);
std::cout << ",\n";
}
std::cout << pad << "}";
}
}, v);
}
六、性能与局限
- 内存占用:
variant的大小等于max(sizeof(T1), sizeof(T2), …) + sizeof(std::size_t),因此对于大对象若不加std::reference_wrapper可能会导致复制代价大。 - 比较与排序:
variant支持operator<,operator==等比较,只要其内部类型都支持相应操作。 - 缺点:
- 对于非常多类型的
variant,编译器可能产生大量模板实例,导致编译时间增长。 - 与
std::any不同,variant需要在编译期确定类型集合,无法动态扩展。
- 对于非常多类型的
七、总结
std::variant 在 C++17 标准中填补了“类型安全多态容器”的空缺,它将运行时类型判定与编译时类型安全结合,提供了简洁、无运行时开销的多态实现方案。无论是状态机、事件系统还是解析 JSON,都能通过 variant 获得更直观、更安全的代码结构。建议在现代 C++ 项目中优先考虑使用 variant,而不是旧式的 union + type_tag 或 std::any。