在 C++17 标准中,std::variant 被引入来解决传统多态(如 void*、std::any、以及类继承层次结构)所带来的类型不安全与性能损失。本文将详细介绍 std::variant 的基本使用、访问方式、遍历访问、与其他 STL 容器结合使用,以及如何借助 std::visit 实现类型安全的多态函数。
1. std::variant 简介
std::variant 是一个可存放多种类型的容器,只能存放 一种 类型的值。它相当于一个“类型安全的” std::union,但提供了更强的类型检查和更丰富的操作接口。
std::variant<int, double, std::string> var;
var = 42; // 存储 int
var = 3.14; // 覆盖为 double
var = std::string("hello"); // 覆盖为 std::string
若想存放自定义类型,必须确保该类型满足 std::variant 的要求:可移动、可拷贝、可比较(可选)等。
2. 访问和检查当前存储类型
2.1 std::get / std::get_if
int i = std::get <int>(var); // 如果 var 当前不是 int,会抛 std::bad_variant_access
int* p = std::get_if <int>(&var); // 若是 int,返回指针,否则返回 nullptr
2.2 std::holds_alternative
if (std::holds_alternative <double>(var)) {
// ...
}
2.3 index() 与 type()
size_t idx = var.index(); // 当前类型的索引,0 表示第一个类型
const std::type_info& ti = var.type(); // 当前类型的 type_info
3. std::visit:访问者模式的现代实现
std::visit 接受一个可调用对象(如 lambda、函数对象、函数指针)和一个或多个 variant,根据 variant 当前存储的类型调用对应的重载版本。
auto visitor = [](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, double>) {
std::cout << "double: " << arg << '\n';
} else {
std::cout << "string: " << arg << '\n';
}
};
std::visit(visitor, var);
如果传入多个 variant,std::visit 会使用 参数包展开 并生成对应组合的重载。
std::variant<int, std::string> a = 10;
std::variant<double, std::string> b = "test";
std::visit([](auto&& va, auto&& vb) {
std::cout << va << " | " << vb << '\n';
}, a, b);
4. 与其他 STL 容器结合
4.1 std::vector<std::variant<...>>
std::vector<std::variant<int, std::string>> vec;
vec.push_back(42);
vec.push_back("hello");
遍历时可使用 std::visit 或 std::get_if。
4.2 递归结构
自定义递归类型时,需要使用 std::variant 包装指针或 std::shared_ptr:
struct Expr;
using ExprPtr = std::shared_ptr <Expr>;
struct Const { int value; };
struct Add { ExprPtr left, right; };
struct Expr : std::variant<Const, Add> {
using variant::variant;
};
这样可以避免无限递归类型定义。
5. 性能考虑
std::variant 的内部实现类似于 union + index。访问时不需要虚函数表,避免了动态分派的开销。
std::visit 的调用会被编译器优化为 switch 或 if-else,并可使用模板递归展开,生成高效代码。
- 与
std::any 相比,std::variant 的类型检查是 编译时 的,能在编译期捕获错误。
6. 常见陷阱
-
缺失类型
若在 variant 中未包含所需的类型,编译器会报错。
std::variant <int> v;
v = 3.14; // 错误
-
未捕获 bad_variant_access
使用 std::get 访问错误类型会抛异常。
try { std::get <double>(v); }
catch (const std::bad_variant_access& e) { /* 处理 */ }
-
递归结构导致堆栈溢出
如前所述,需要使用指针包装递归类型。
7. 典型应用场景
| 场景 |
传统实现 |
std::variant 实现 |
优点 |
| 消息系统 |
基于 enum + union + 手动 switch |
variant + visit |
更安全、易扩展 |
| 解析树 |
多个类继承自基类 |
递归 variant |
消除虚表,降低内存占用 |
| 配置文件 |
std::map<std::string, boost::variant> |
std::variant |
省去第三方库,标准化 |
| UI 事件 |
std::variant |
std::variant |
统一事件类型,避免类型转换 |
8. 小结
std::variant 为 C++ 提供了 类型安全的代替 union,与 std::visit 的配合实现了 现代多态 的高效方案。相比传统的 void*、std::any 或继承层次结构,variant 在类型检查、性能和易用性上都有显著提升。通过本文的示例与技巧,相信读者已经具备使用 std::variant 进行安全多态编程的基本能力。