在 C++17 之前,程序员常常使用 void* 或者基类指针来实现多态或容器的通用类型存储。随着标准库的完善,std::any 和 std::variant 两个类型提供了更安全、更类型化的替代方案。它们分别解决了不同的需求:std::any 用于存放任意类型的值,而 std::variant 用于在已知的有限类型集合中存放值。本文将从定义、语义、使用场景、性能和常见错误等角度深入探讨两者的区别,并给出一些实用的编码建议。
1. 基本定义
| 特性 | std::any |
std::variant |
|---|---|---|
| 类型安全 | 运行时检查 | 编译时检查 |
| 值范围 | 任意类型 | 预先声明的有限类型集合 |
| 典型用途 | 动态类型存储、泛型容器 | 取值多态、模式匹配 |
| 成员函数 | type(), any_cast, has_value |
index(), std::get, std::visit |
| 默认构造 | 空 | 第一个类型的默认值 |
2. 语义差异
2.1 类型安全
std::any:在存储值时不做任何类型检查,只有在取值时通过 `any_cast ` 才能确认是否为期望类型。若类型不匹配则抛出 `std::bad_any_cast`。std::variant:在编译阶段就确定可存储的类型集合,任何操作都必须符合该集合。访问时通过index()或 `std::get `,若索引不匹配会触发 `std::bad_variant_access`。
2.2 存储方式
std::any采用“类型擦除”技术,内部使用std::type_info和一个基类指针来存放实际对象。所有赋值/拷贝都需要动态分配(如有必要)。std::variant采用“联合”方式(std::aligned_union),所有候选类型共享同一块内存,只有一个类型的构造函数被调用。内存管理更简单、开销更小。
3. 性能对比
| 维度 | std::any |
std::variant |
|---|---|---|
| 构造/赋值 | 可能需要堆分配 | 通常为栈分配 |
| 访问 | 需要运行时判断 | 编译时确定 |
| 复制 | 需要复制任意类型 | 只复制活跃类型 |
| 对齐 | 动态 | 静态对齐 |
结论:若已知类型集合且不需要动态添加类型,
std::variant更快、更安全。若需要完全通用的容器,使用std::any更灵活。
4. 常见使用场景
4.1 std::any
- 事件系统:将事件参数以
any存放,事件处理函数通过any_cast提取所需类型。 - 插件架构:插件向宿主提供多种类型的返回值,宿主通过
any统一处理。 - 键值存储:类似 Python 的
dict,键对应任意类型的值。
std::any store;
store = 42; // int
store = std::string("abc"); // string
try {
std::cout << std::any_cast<int>(store) << '\n';
} catch(const std::bad_any_cast&) {
std::cerr << "类型不匹配\n";
}
4.2 std::variant
- 解析器:AST 节点可以是多种类型,使用 variant 统一存储。
- 配置系统:配置值可为
int,double,std::string等,variant 提供类型安全访问。 - 状态机:状态可以是
Idle,Running,Paused等几种枚举类型,用 variant 表示。
using Value = std::variant<int, double, std::string>;
Value v = 3.14;
std::visit([](auto&& arg){ std::cout << arg << '\n'; }, v);
5. 编码技巧与陷阱
| 技巧 | 说明 |
|---|---|
std::variant 的 monostate |
用作空值,类似 null |
访问 index() |
可以在 switch 语句中进行模式匹配 |
std::visit 与 std::variant |
可以使用 lambda 表达式或 std::apply 结合 std::tuple |
std::any 的 has_value() |
在尝试 any_cast 前检查是否为空 |
失效的 any_cast |
在多线程环境下,需要使用锁或 atomic_any(自定义)来避免数据竞争 |
variant 的非活跃成员析构 |
std::variant 只析构当前激活的成员,避免多余析构开销 |
6. 代码示例:多态事件系统
#include <any>
#include <variant>
#include <vector>
#include <iostream>
#include <string>
using EventData = std::variant<int, std::string, double>;
struct Event {
int type; // 事件类型
EventData data; // 事件携带数据
};
class EventBus {
public:
void publish(const Event& e) {
subscribers_.push_back(e);
}
void process() {
for(const auto& ev : subscribers_) {
std::visit([&ev](auto&& val){
using T = std::decay_t<decltype(val)>;
if constexpr (std::is_same_v<T, int>) {
std::cout << "int: " << val << '\n';
} else if constexpr (std::is_same_v<T, std::string>) {
std::cout << "string: " << val << '\n';
} else if constexpr (std::is_same_v<T, double>) {
std::cout << "double: " << val << '\n';
}
}, ev.data);
}
subscribers_.clear();
}
private:
std::vector <Event> subscribers_;
};
int main() {
EventBus bus;
bus.publish({1, 42});
bus.publish({2, std::string("hello")});
bus.publish({3, 3.14});
bus.process();
}
7. 结语
std::any是通用、动态类型存储的理想选择,适合需要极大灵活性的场景,但需要在运行时承担类型检查与错误处理的成本。std::variant则在已知有限类型集合时提供更高的性能与更强的类型安全,是实现模式匹配、状态机和多态 AST 等的首选。
在实际项目中,往往两者会并存:variant 用于内部实现的有限多态,而 any 用于插件接口或配置系统。熟练掌握两者的语义与使用方法,能够让你的 C++ 代码既安全又高效。