在 C++17 中,标准库提供了两种常见的类型擦除容器:std::variant 与 std::any。它们看起来相似,但在语义、类型安全、性能以及使用场景上存在显著差异。本文将从定义、类型安全、操作方式、性能以及典型使用案例等角度,对二者进行深入比较,并给出实际编码建议。
1. 基本定义
| 关键字 | 说明 |
|---|---|
std::any |
允许存放任意类型的值,但在编译期无法获知存放的具体类型。 |
std::variant |
只能存放预先指定的一组类型之一,编译期已知类型集合。 |
std::any 的实现类似于类型擦除(type erasure),内部使用动态分配存储对象,并记录其完整类型信息;std::variant 则使用 union 与 std::variant_alternative 机制,采用位域记录当前值的类型索引。
2. 类型安全
- std::any:在取值时需要使用 `any_cast ` 指定期望类型,如果实际类型不匹配会抛出 `std::bad_any_cast`。此过程在运行时检查,编译器无法提前捕获错误。
- std::variant:通过 `std::get ` 或 `std::get_if` 访问值,如果类型不匹配会抛出 `std::bad_variant_access` 或返回 `nullptr`。由于 `variant` 的类型集合已在编译期确定,编译器可以在许多情况下对访问路径进行检查,减少运行时错误。
3. 性能
| 维度 | std::any | std::variant |
|---|---|---|
| 内存布局 | 需要动态分配(heap)或至少使用 SSO(small string optimization) | 只在栈上存储固定大小的内存,避免堆分配 |
| 运行时检查 | 需要完整的 RTTI 以及异常抛掷 | 仅需要索引比较,异常处理更轻量 |
| 适配器 | 需要 typeid、any_cast 的模板匹配 |
需要 variant 的 visit 或 get 语义,访问成本更低 |
综上,若性能是关键因素且类型集合已知,std::variant 更优;若类型未知或需要高度动态的对象存储,std::any 仍有价值。
4. 使用场景
| 场景 | 推荐容器 |
|---|---|
| 插件系统:对象类型多且未知 | std::any 或自定义 type-erased base |
| 事件系统:事件类型固定且多 | std::variant |
| 调试信息:存放多种调试对象 | std::any |
| 状态机:有限状态机中的状态类 | std::variant |
| 数据持久化:序列化不同字段 | std::variant 与 visitor |
| 跨语言接口:不确定类型 | std::any 或 boost::any |
5. 代码示例
5.1 std::any
#include <any>
#include <iostream>
#include <string>
int main() {
std::any a = 10; // 存放 int
a = std::string{"hello"}; // 替换为 string
try {
std::cout << std::any_cast<int>(a) << '\n'; // 抛异常
} catch(const std::bad_any_cast& e) {
std::cout << "bad_any_cast: " << e.what() << '\n';
}
std::cout << std::any_cast<std::string>(a) << '\n';
}
5.2 std::variant
#include <variant>
#include <iostream>
#include <string>
#include <vector>
int main() {
std::variant<int, std::string, std::vector<int>> v = 42;
// 访问
std::cout << std::get<int>(v) << '\n';
// 访问失败
try {
std::cout << std::get<std::string>(v) << '\n';
} catch(const std::bad_variant_access& e) {
std::cout << "bad_variant_access: " << e.what() << '\n';
}
// visitor
std::visit([](auto&& arg){
std::cout << "value: " << arg << '\n';
}, v);
// 变换为 vector
v = std::vector <int>{1,2,3};
std::visit([](auto&& arg){
std::cout << "vector size: " << arg.size() << '\n';
}, v);
}
6. 与 boost::variant / std::any 的对比
boost::variant与std::variant功能相近,但boost::variant在 C++11 时就出现,支持更旧的编译器。std::variant在性能与标准兼容性方面更好。boost::any与std::any同理。若项目已使用 Boost,可根据需求保留或迁移。
7. 进阶技巧
- 自定义 visitor:利用
std::variant的visit可以轻松实现多态处理。 - 默认值:
std::variant可以在构造时指定默认类型,使用std::variant<T...> v;时默认值是第一个类型的默认构造。 - std::monostate:可作为占位符,让
variant在空状态下返回默认值。 - 异常安全:
variant的emplace与operator=在强异常安全保证下完成。
8. 小结
- std::any:适用于需要动态、类型未知存储的场景;提供最少的类型信息,使用时需手动检查类型并处理异常。
- std::variant:适用于类型集合已知、需要高性能或类型安全的场景;编译期已确定类型,访问更安全、效率更高。
在实际项目中,常见的做法是将 std::variant 用于内部实现(例如事件或状态机),而将 std::any 用于插件接口或外部 API 的参数传递。合理选择与组合,可让 C++ 代码既灵活又高效。