在 C++17 标准中,std::variant 和 std::any 两个新容器极大地方便了类型安全和类型擦除的实现。它们虽然都能在运行时容纳不同类型的值,但本质上设计目标截然不同。本文将从定义、使用方式、性能、错误处理以及实际应用案例四个维度深入剖析两者,帮助开发者根据具体需求做出最合适的选择。
1. 基本定义与核心语义
| std::variant | std::any | |
|---|---|---|
| 语义 | 类型安全的和(sum type) | 类型擦除(任何类型的盒子) |
| 编译期类型 | 在模板参数列表中显式列出 | 在编译期不知晓,运行时才确定 |
| 类型安全 | 通过 `std::holds_alternative | |
、std::get、std::visit检测/访问 | 通过any_cast` 进行类型检查,若不匹配抛出异常 |
||
| 主要用途 | 需要在有限种类型中安全切换、模式匹配 | 需要在运行时存放任意类型、实现泛型接口或消息总线 |
核心区别:
variant是一个“有限的”多态容器,所有可能的类型必须在编译时确定;any则是一个“一般的”类型擦除容器,能容纳任何类型,甚至是非标准类型。
2. 典型使用场景对比
2.1 std::variant
- 配置或选项:例如
std::variant<int, double, std::string>可以表示用户输入的数值或字符串。 - 状态机:状态树中的节点可通过
variant保存不同形态的数据结构。 - 网络消息:对不同协议的数据包使用
variant,配合std::visit实现多态处理。 - 返回值包装:像
std::optional、std::expected(C++23)一样,将错误码、成功值或失败信息包装在一个variant中。
2.2 std::any
- 插件/事件系统:事件总线传递任意类型的数据,监听者通过
any_cast解析。 - 动态属性:对象拥有可动态添加/修改的属性,每个属性可以是任意类型。
- 跨库接口:当两个库彼此不共享类型定义时,可以通过
any传递数据。 - 泛型容器:实现一个“多类型”容器,内部使用
std::any存储不同类型的元素。
3. 性能与资源管理
3.1 结构体大小
- variant:其大小为
max(sizeof(T1), sizeof(T2), …) + sizeof(size_t),所有成员共用同一块内存空间。 - any:内部一般为
sizeof(void*) * 2 + sizeof(size_t),包括指针、类型信息等;若对象较大会使用堆分配。
3.2 拷贝与移动
- variant:复制和移动是值语义,按成员类型的拷贝/移动构造实现。
- any:复制会调用内部
clone(),若对象是 POD 直接拷贝,否则需要 heap 分配;移动则通常是浅拷贝,保持指针不变。
3.3 异常安全
- variant:在赋值过程中如果内部构造失败,
variant会保持旧状态。 - any:
any_cast若类型不匹配会抛出bad_any_cast,不影响存储的对象。
4. 常见陷阱与最佳实践
| 场景 | 说明 | 解决方案 |
|---|---|---|
variant 中的类型重复 |
std::variant<int, int> 编译错误 |
移除重复类型 |
any_cast 失败 |
忘记检查类型 | `any_cast |
(&any_obj)或使用any_cast` 后捕获异常 |
||
| 大对象复制 | variant 复制大对象时效率低 |
采用 `std::shared_ptr |
或std::unique_ptr` 包装 |
||
| 多线程共享 | any 对象在多线程环境下未加锁 |
通过 std::mutex 或 std::atomic<std::shared_ptr<any>> 进行同步 |
4.1 代码示例:状态机
using State = std::variant<Idle, Running, Paused, Error>;
void process(State& s) {
std::visit([](auto&& state){
using T = std::decay_t<decltype(state)>;
if constexpr (std::is_same_v<T, Idle>) { /* 处理 Idle */ }
else if constexpr (std::is_same_v<T, Running>) { /* 处理 Running */ }
else if constexpr (std::is_same_v<T, Paused>) { /* 处理 Paused */ }
else if constexpr (std::is_same_v<T, Error>) { /* 处理 Error */ }
}, s);
}
4.2 代码示例:事件总线
class EventBus {
std::unordered_map<std::string, std::vector<std::function<void(std::any)>>> subs_;
public:
template<typename T>
void subscribe(const std::string& topic, std::function<void(const T&)> cb) {
subs_[topic].push_back([cb = std::move(cb)](std::any a) {
if (auto ptr = std::any_cast <T>(&a))
cb(*ptr);
});
}
template<typename T>
void publish(const std::string& topic, const T& data) {
if (auto it = subs_.find(topic); it != subs_.end()) {
std::any a = data;
for (auto& f : it->second)
f(std::move(a));
}
}
};
5. 结语
- 当你需要 受限类型集合、编译时类型安全、高性能 时,选择
std::variant。 - 当你需要 任意类型、动态扩展、跨模块数据传递 时,选择
std::any。
二者并不互斥,实际项目中往往会在同一个代码库里同时使用。掌握它们的语义、性能特性以及正确的使用模式,是现代 C++ 开发者提升代码质量与可维护性的关键之一。