在 C++17 之后,标准库新增了两种在运行时实现类型安全的容器——std::variant 和 std::any。它们都可以用来存放不同类型的值,但在使用场景、类型安全性、性能以及语义上存在显著差异。本文从概念、类型安全、异常安全、使用方法、性能比较以及实际应用场景等方面,对两者进行系统对比,并给出在不同业务需求中如何选择的建议。
1. 概念对照
| 项目 | std::variant |
std::any |
|---|---|---|
| 定义 | 一种可变的、静态类型安全的多态容器;其模板参数列表 Variant<Types...> 预先声明所有可能的类型 |
一种动态类型安全的容器;可以存放任意类型,类型在运行时决定 |
| 类型检查 | 编译期检查,使用 `std::holds_alternative | |
()、std::get()等 | 运行时检查,使用typeid或any_cast()` |
||
| 内存分配 | 只在内部使用固定大小的缓冲区(max(sizeof(T1), sizeof(T2), …)),不产生堆分配(除非类型自身需要堆) |
采用内部 std::any::holder 对象,通常会分配堆内存来保存存储的数据 |
| 析构 | 通过 variant 的类型信息在销毁时调用正确的析构函数 |
同样通过 any 的类型信息调用析构,但在堆上分配的对象需要额外的堆析构 |
2. 类型安全与可读性
2.1 std::variant:编译期安全
- 类型已知:编译器在编译时就知道
variant能容纳哪些类型,所有访问都在编译期校验。 - 错误提示更友好:如果你尝试访问一个不在模板参数列表中的类型,编译器会给出错误,避免运行时崩溃。
- 可读性更高:代码结构类似枚举,读者能一眼看出变量可能的类型。
2.2 std::any:运行时安全
- 灵活性更高:容器可以在任何时间存放任何类型,适用于不确定类型序列化、插件系统、事件总线等场景。
- 错误隐蔽:如果你 `any_cast ()` 的类型不匹配,默认抛出 `std::bad_any_cast`,但如果你忽略异常或使用 `any_cast(&any)` 返回指针,会得到 `nullptr`,这在业务逻辑中可能被误认为是合法的空值。
3. 性能对比
| 指标 | std::variant |
std::any |
|---|---|---|
| 内存占用 | 固定大小,取最大类型大小(再加对齐填充) | 需要堆内存,且存储对象大小与实际对象大小相同 |
| 访问成本 | `std::get | |
()只做索引和偏移 |any_cast()` 需要检查类型、动态分配或拷贝 |
||
| 拷贝/移动 | 直接调用对应类型的拷贝/移动构造 | 需要通过类型擦除机制进行构造,可能更耗时 |
| 编译时间 | 对模板参数数量敏感,太多类型会导致编译慢 | 对类型的依赖更弱,编译时间相对稳定 |
总结:在高性能、频繁访问的场景,variant 更有优势;在类型不确定或需要动态扩展的业务,any 更合适。
4. 典型使用案例
4.1 std::variant
-
实现有限状态机
struct Loading {}; struct Success { int data; }; struct Error { std::string msg; }; using State = std::variant<Loading, Success, Error>; State s = Loading{}; - 多态函数返回值
std::variant<int, std::string> parse(const std::string& s) { try { return std::stoi(s); } catch (...) { return s; } } - 树形结构(如 JSON)
using JsonValue = std::variant<std::nullptr_t, bool, int, double, std::string, std::vector <JsonValue>, std::unordered_map<std::string, JsonValue>>;
4.2 std::any
- 插件系统的参数容器
std::unordered_map<std::string, std::any> settings; settings["threshold"] = 0.85f; settings["mode"] = std::string("fast"); - 事件总线(Event Bus)
void dispatch(std::any payload) { if (auto p = std::any_cast <int>(&payload)) { handleInt(*p); } else if (auto p = std::any_cast<std::string>(&payload)) { handleString(*p); } else { handleUnknown(payload); } } - 通用缓存
std::unordered_map<std::string, std::any> cache; cache["user"] = User{...}; cache["config"] = Config{...};
5. 何时选择哪一个?
| 需求 | 推荐容器 |
|---|---|
| 类型范围已知且固定 | std::variant |
| 需要在编译期对类型做判定 | std::variant |
| 需要高性能、频繁访问 | std::variant |
| 类型不确定、可在运行时动态添加 | std::any |
| 与反射、序列化框架集成 | std::any |
| 需要跨模块传递“任意”类型的数据 | std::any |
| 需要保证在运行时不出现类型错误(如插件调用) | std::any(配合异常捕获) |
6. 代码小技巧
6.1 std::variant 的访问
// 访问多重可能类型
std::visit([](auto&& arg){
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, Success>) {
std::cout << "Success: " << arg.data << '\n';
} else if constexpr (std::is_same_v<T, Error>) {
std::cout << "Error: " << arg.msg << '\n';
}
}, state);
6.2 std::any 的安全检查
if (auto p = std::any_cast <int>(&anyObj)) {
std::cout << "int: " << *p << '\n';
} else {
std::cout << "not int\n";
}
6.3 性能微调
- 对
std::variant,在模板参数列表中使用std::monostate作为空状态,避免不必要的构造成本。 - 对
std::any,在需要大量拷贝时,可以考虑使用any_cast<std::reference_wrapper<T>>来避免复制。
7. 小结
std::variant:编译期类型安全、性能优秀、适用于已知有限类型集合的场景。std::any:运行时类型安全、极高的灵活性、适用于类型不确定或需要动态扩展的业务。
了解并灵活运用这两者,能让 C++ 代码在类型安全与灵活性之间找到最佳平衡点。希望本文能帮助你在项目中更好地决定使用哪种容器。