在 C++17 之前,如果需要在同一变量中存放不同类型的数据,通常会采用 std::any 或 boost::variant。这两种方案虽然可行,但各有不足:std::any 失去了编译期类型检查的优势,而 boost::variant 在语法上仍然显得笨重。C++17 的 std::variant 则把这两者的优点结合在一起,提供了一种既安全又简洁的方式来处理多种可能类型。
1. 基本概念
std::variant 是一个类型安全的联合体(variant type),它内部存储一组预先定义好的类型之一。使用时必须明确当前存储的具体类型,若访问错误类型会抛出 std::bad_variant_access 异常。
#include <variant>
#include <string>
#include <iostream>
std::variant<int, std::string> v = 42; // 存储 int
v = std::string("hello"); // 现在存储 std::string
2. 访问方式
2.1 std::get
直接通过索引或类型访问当前值:
int i = std::get <int>(v); // 若 v 当前不是 int,则抛异常
std::string s = std::get<std::string>(v);
2.2 std::get_if
返回指向当前值的指针,若类型不匹配则返回 nullptr:
if (auto p = std::get_if <int>(&v)) {
std::cout << *p << '\n';
}
2.3 std::visit
更灵活的访问方式,类似多态 dispatch:
std::visit([](auto&& arg){ std::cout << arg << '\n'; }, v);
3. 与传统联合体的比较
- 类型安全:
std::variant通过模板参数限制可存储的类型,避免了错误类型访问。 - 异常安全:在访问错误类型时抛异常,能让错误在运行时被捕获。
- 复制与移动:
std::variant通过内部维持活跃成员的状态,支持浅拷贝与移动。
4. 典型应用场景
4.1 解析 JSON
在解析 JSON 数据时,可以使用 std::variant 来表示不同的值类型(数值、字符串、布尔值、数组、对象)。
using JsonValue = std::variant<
std::nullptr_t,
bool,
int64_t,
double,
std::string,
std::vector <JsonValue>,
std::map<std::string, JsonValue>
>;
4.2 消息总线
在一个事件系统里,事件类型可能多种多样,使用 std::variant 作为事件数据结构,可以在同一槽中统一处理不同事件。
5. 性能考虑
std::variant 的大小等于其内部类型中最大类型的大小加上一些状态信息(通常是一个 unsigned 值)。相比 std::any 的类型擦除实现,std::variant 的访问速度更快,因为不需要运行时类型信息(RTTI)。不过,对于非常大或复杂的类型组合,复制成本仍可能较高,需要根据实际使用进行评估。
6. 小结
std::variant 在 C++17 标准中为多态值提供了一个简洁、类型安全且性能优越的实现方案。它将传统联合体的灵活性与类型安全结合起来,成为现代 C++ 开发中处理多种可能值的首选工具。掌握 std::variant 的使用技巧后,你将能在需要类型多样化的场景中写出更稳健、更易维护的代码。