varient与any
1. std::variant (变体)
std::variant 是一个类型安全的联合体 (union)。它可以在编译时定义一个固定且有限的类型集合,并在运行时精确地持有其中一个类型的值。你总是知道 std::variant 可能包含哪些类型。
核心思想: “这个变量要么是一个 int,要么是一个 double,要么是一个 std::string,但它在任何时候都只可能是其中一个。”
特点:
- 编译时已知类型集: 在声明
std::variant时,你需要明确列出它可能包含的所有类型。 - 类型安全: 你不能意外地从
std::variant中提取一个它当前不持有的类型的值。如果尝试这样做,会抛出std::bad_variant_access异常或返回nullptr。 - 空间效率: 通常会在栈上分配足够的空间来存储其所有可能类型中最大的那个,避免了动态内存分配(除非内部类型本身需要动态分配)。
- 无额外开销: 相比于多态(虚函数),
std::variant通常没有虚函数表的开销。 - 访问方式:
std::get<T>(variant_obj):按类型获取值,如果类型不匹配则抛出std::bad_variant_access。std::get<index>(variant_obj):按索引获取值,如果索引不匹配则抛出std::bad_variant_access。std::get_if<T>(&variant_obj):按类型获取值的指针,如果类型不匹配则返回nullptr。std::visit(callable, variant_obj...):这是最强大和推荐的访问方式。它接受一个或多个callable对象(例如 lambda 表达式、函数对象)和一个或多个std::variant对象,然后根据variant当前持有的类型调用相应的callable重载。这是一种类型安全的访问模式,类似于访问者模式。
- 空状态: 默认构造的
std::variant会持有其第一个模板参数类型的默认构造值。如果你想表示一个“空”或“未初始化”的状态,可以使用std::monostate作为它的第一个类型。
何时使用 std::variant:
- 当你有一个有限且已知的类型集合,并且变量在任何时候只能是其中一个。
- 实现状态机。
- 函数返回多种可能的结果类型(例如
std::expected的底层实现)。 - 解析抽象语法树 (AST) 节点。
- 替代类型不安全的 C 风格联合体。
示例:
1 |
|
2. std::any (任意类型)
std::any 是一个类型安全的容器,它可以存储任何单个值,只要该类型是可复制构造 (CopyConstructible) 的。你不需要在编译时知道它可能包含哪些类型,它在运行时处理类型信息。
核心思想: “这个变量可以包含任何东西,但它在任何时候都只包含一个东西。我需要在使用时猜测并验证它里面是什么。”
特点:
- 运行时未知类型: 你不需要在声明
std::any时指定它可能包含的类型。它可以在运行时存储任何可复制构造的类型。 - 类型擦除 (Type Erasure):
std::any通过类型擦除技术来存储任意类型的数据。它内部维护了存储值的副本以及其类型信息。 - 动态内存分配: 通常会涉及动态内存分配来存储其内部的值,尤其当存储的对象大小超过某个阈值时(小对象优化可能会在栈上)。
- 运行时类型检查: 访问
std::any中的值需要进行运行时类型检查。 - 访问方式:
std::any_cast<T>(any_obj):尝试将any_obj中的值转换为类型T。如果any_obj为空或存储的类型不是T,则抛出std::bad_any_cast异常。std::any_cast<T>(&any_obj):尝试将any_obj中的值转换为类型T的指针。如果any_obj为空或存储的类型不是T,则返回nullptr。any_obj.has_value():检查any_obj是否包含值。any_obj.type():返回std::type_info对象,提供关于存储类型的信息(通常用于调试或更复杂的类型检查)。
何时使用 std::any:
- 当你需要存储真正任意的类型,并且在编译时无法预知所有可能的类型。
- 实现插件系统,插件可以返回任意类型的数据。
- 配置系统,配置项的值可以是各种类型。
- 事件系统,事件携带的数据可以是任意类型。
- 需要传递任意上下文数据到通用函数中。
示例:
1 |
|
std::variant vs std::any 总结对比
| 特性 | std::variant |
std::any |
|---|---|---|
| 类型集合 | 编译时已知,固定,有限 | 运行时未知,任意 (任何 CopyConstructible 类型) |
| 安全性 | 编译时安全 (通过 std::visit 和类型列表) |
运行时安全 (通过 std::any_cast 检查) |
| 性能/开销 | 通常更高,无动态分配(除非内部类型大),无类型擦除开销 | 通常较低,涉及动态分配和类型擦除开销 |
| 内存分配 | 通常在栈上,大小为最大成员类型的大小 | 通常在堆上(对于大对象),涉及动态内存管理 |
| 访问方式 | std::get, std::get_if, std::visit (推荐) |
std::any_cast (可能抛异常或返回 nullptr) |
| 适用场景 | 有限的、已知的、固定的几种类型选择 | 任意的、未知的类型,需要极致的灵活性 |
| 空状态 | 默认持有第一个类型的值,可使用 std::monostate |
has_value() 为 false 时为空,或调用 reset() 清空 |
简单来说:
- 如果你知道所有可能的类型,并且这个集合是固定的,使用
std::variant。它提供了更好的编译时安全性和性能。 - 如果你需要存储任何类型的数据,并且在编译时无法预知所有可能性,使用
std::any。它提供了最大的灵活性,但代价是运行时开销和更多的运行时类型检查。
All articles on this blog are licensed under CC BY-NC-SA 4.0 unless otherwise stated.
Comments


