C++ std::any 与 std::variant 是什么?(如何替代传统的 void* 和 union)

2次阅读

std::any用于运行时未知类型的单值存储,依赖动态类型检查;std::variant适用于编译期已知有限类型的高效安全切换,需全覆盖处理。选any当类型完全不确定,选variant当类型集合固定且需高性能分发。

C++ std::any 与 std::variant 是什么?(如何替代传统的 void* 和 union)

std::any 能存任意类型,但取值前必须知道类型

它像一个带类型擦除的“万能盒子”,运行时才检查类型是否匹配。和 void* 不同,std::any 自带类型信息,不会让你在解引用时崩溃——但如果你猜错了类型,std::any_cast 会抛出 std::bad_any_cast 异常。

常见错误现象:std::any_cast<int>(a)</int> 对一个存了 doublestd::any 调用,直接崩溃(没捕获异常);或者用 std::any_cast<const int></const> 去取一个右值,编译失败。

  • 使用场景:配置项解析、插件系统中传递未知但单次确定的值(比如某个字段可能是 intstd::String,但具体哪种由 json 字段名决定)
  • 安全取值必须先用 std::any_cast<t>(&a)</t> 检查是否可转,或用 std::any_cast<t>(a)</t> 并包 try/catch
  • 性能影响:每次 std::any_cast 都要动态类型比对,比直接访问慢;内部可能分配(小对象优化取决于实现)
  • 别把它当 union 用——它不支持多类型共存,也不提供 switch-case 式的类型分发

std::variant 是编译期限定的“安全 union”

std::variant 在定义时就锁死了能存哪些类型,比如 std::variant<int std::string double></int>。它比原始 union 安全得多:构造、析构、赋值都自动管理活跃成员,不会出现“用 int 覆盖 string 后还调 string 析构函数”的 UB。

常见错误现象:忘记处理所有分支,std::visit 传入的 visitor 缺少对某个类型的重载,导致编译失败;或者用 std::get<t>(v)</t> 强制取值,而当前活跃类型不是 T,抛出 std::bad_variant_access

立即学习C++免费学习笔记(深入)”;

  • 使用场景:状态机返回值(成功/失败/超时)、AST 节点类型(BinaryOp/Literal/Identifier)、协议字段的多种可能取值
  • 必须用 std::visit 处理所有可能类型,推荐用 Lambda 模板或 std::overload 辅助类组织逻辑
  • 参数差异:std::get_if<t>(&v)</t> 返回指针,安全但需判空;std::get<t>(v)</t> 更快但不安全,只应在确定类型后使用
  • 内存布局紧凑,无堆分配,访问开销接近原生 union,但多了 1–2 字节的 type index

什么时候选 any,什么时候选 variant?

关键看“类型集合”是编译期可知,还是运行时才确定。

  • std::any:你根本不知道将来会塞什么类型(比如用户脚本传进来的值),且只存不频繁切换
  • std::variant:你知道全部可能类型,且需要高效、无异常地分发处理(比如解析器每步只产出几种固定节点)
  • 别用 std::any 模拟枚举行为——类型太多会导致运行时检查膨胀,也难维护
  • 别用 std::variant 存 “任意类型”,比如 std::variant<:any ...></:any>,这等于放弃类型安全又没换来灵活性

替代 void* 和 union 的真实代价

它们确实消除了裸指针的悬垂风险和 union 的手动生命周期管理,但引入了新约束:类型必须明确、可比较、可拷贝(或移动)。尤其要注意 std::variant 中的类型不能是抽象类、数组或带删除拷贝构造的类型。

  • void* 的自由是以放弃所有类型检查为代价的;std::anystd::variant 把检查移到了更早阶段——一个在运行时,一个在编译时
  • 兼容性上,std::any 要求 c++17,std::variant 同样;老项目升级时注意 MSVC/GCC/Clang 版本是否支持完整特性(比如 Clang 5+ 才完全支持 std::variant 的 constexpr 构造)
  • 最容易被忽略的一点:std::variantstd::monostate 占位符不是摆设——它让 variant 可默认构造,否则所有类型都得有默认构造函数,这点在设计接口时经常卡住
text=ZqhQzanResources