C++ STL 之 optional、variant 与 any 详解

发布时间:2026/6/30 23:50:29

C++ STL 之 optional、variant 与 any 详解 C STL 之 optional、variant 与 any 详解一、问题C 类型系统在 C17 之前有三大短板函数返回值无法优雅表达可能无值——传统做法用指针可能空悬或哨兵值如-1、nullptr语义含混且易误用需要要么 A 要么 B的类型安全变体——只能用union但原始union不检查活跃成员读取错误成员属于未定义行为需要持有任意类型的值完全的动态类型——过去只能退化为void* 手动 RTTI或继承方案C17 引入的std::optional、std::variant、std::any分别补上了这三个短板。二、optional可能无值的语义2.1 基本用法#includeoptional#includeiostream#includestringstd::optionalintparse_int(conststd::strings){try{returnstd::stoi(s);}catch(...){returnstd::nullopt;// 无值}}intmain(){autor1parse_int(42);autor2parse_int(abc);// has_valuestd::coutr1.has_value();// 1std::coutr2.has_value();// 0// value_or 提供默认值intvr2.value_or(-1);// -1// operator* / operator-没有安全检查需自己保证 has_valueif(r1){std::cout*r1;// 42}// value() 会抛 bad_optional_access// r2.value(); // 抛异常}2.2 注意事项optionalT会存储额外的bool标记如果T自身可以被默认构造为空状态如std::string不一定比哨兵值更省optionalT是 C17 不支持的C23 才引入引用包装推荐用std::reference_wrapper或裸指针value_or的参数是按值传入的即使T是移动成本高的类型也会拷贝注意用std::move或手动短路if(opt)三、variant类型安全的联合体3.1 基本用法#includevariant#includestring#includeiostream// 定义一个 要么 int 要么 string 的类型std::variantint,std::stringv42;// 赋值另一个类型vhello;// 获取值——类型不匹配时抛 bad_variant_accessintistd::getint(v);// 抛异常当前是 stringstd::string sstd::getstd::string(v);// OK// get_if 返回指针失败返回 nullptrif(auto*pstd::get_ifint(v)){std::cout*p;}else{std::cout不是 int;}3.2 std::visit编译期分派std::visit是 variant 的核心武器——它根据 variant 当前活跃的索引在编译期生成所有分支的分派表运行时只需查偏移autovisitor[](autoarg){usingTstd::decay_tdecltype(arg);ifconstexpr(std::is_same_vT,int)std::coutint: arg;elseifconstexpr(std::is_same_vT,std::string)std::coutstring: arg;};std::visit(visitor,v);// 编译期分派零虚函数开销对比虚函数方案std::visit的分派时间与 variant 的备选类型数量是 O(1) 的查索引跳转表而虚函数需要一次虚表查址加一次间接调用。两种都很便宜但 visit 不需要类层次继承组合更灵活。3.3 variant vs 传统 union读 a读 b是否union 声明赋值成员 a读取哪个成员?正确未定义行为程序员自行跟踪活跃成员variant 声明赋值 stringget?抛 bad_variant_accessget 成功编译期类型安全3.4 注意事项variant的默认构造会构造第一个类型的默认值如果第一个类型不可默认构造variant 也不能默认构造。可以用std::monostate作为第一备选来做空语义访问不活跃成员抛异常std::get抛std::bad_variant_accessstd::get_if返回nullptrvalueless_by_exception()赋值时如果异常发生variant 可能进入无值状态一般情况下应避免让其进入此态四、any类型擦除的通用容器4.1 基本用法#includeany#includeiostream#includestringstd::any a42;// 存 intastd::string(hello);// 存 stringa3.14;// 存 double// 取出时必须类型完全一致try{doubledstd::any_castdouble(a);// OKintistd::any_castint(a);// 抛 bad_any_cast}catch(conststd::bad_any_caste){std::coute.what();}// any_cast 指针版本失败返回 nullptrif(auto*pstd::any_castdouble(a)){std::cout*p;}4.2 性能代价std::any的实现内部做两次动态分配堆 类型擦除包装器对于小对象通常 ≤ 32 字节且可平凡拷贝实现可能用 SBO小对象优化避免堆分配但不可平凡拷贝的大对象一定堆分配每次std::any_castT内部通过typeid比较运行时类型信息RTTI有运行时代价与optional零堆分配和variant栈上存储 编译期相比any是最重的方案结论除非必须持有运行时才能确定的任意类型否则优先用 optional 或 variant。五、选型指南是否是否是否场景需要持有什么类型的值?运行时类型完全已知?可能无值?std::anystd::optional类型数量有限且已知?std::variant语义可能有值也可能没有例解析结果、查找返回值语义固定几个类型中的某一个例AST 节点、消息类型语义运行时才能确定的任意类型例插件系统、序列化反序列化六、面试题Q1optionalT和裸指针有什么区别A裸指针可以表达空nullptr但有歧义——它是没有值还是指向的对象被销毁optional语义明确且支持value_or、has_value、and_thenC23等组合子不可空悬。Q2optionalT的空间开销是多少A额外一个bool通常 1 字节 padding对齐由T决定。如果T自身有空状态如std::string的空字符串optional并不比带哨兵的版本省空间但语义更清晰。Q3std::visit比虚函数快吗A实测两者在同一量级。std::visit通过索引跳转表 O(1) 分派但编译器倾向于把虚函数内联。关键区别在组合variantvisit不需要类层次新增类型只需扩展 variant 类型参数不侵入已有代码。Q4variant的空状态是怎么回事A默认构造构造第一个类型如果赋值时发生异常variant 可能进入valueless_by_exception()为true的状态此时任何get和visit都会抛异常。可以用std::monostate作为第一类型占位 0 字节来手动管理空状态。Q5std::any为什么比variant慢A因为any做类型擦除用到虚函数包装器和堆分配且any_cast依赖typeid运行时比较variant的所有备选类型编译期已知存储在栈上分派是编译期构造的索引表。Q6请用合适的工具实现一个能存int、double、string之一的类型应选variant还是any为什么A选variantint, double, string。因为类型集合编译期完全确定variant零堆分配、O(1) 分派、类型安全。any会引入不必要的动态分配和 RTTI 查询。Q7optional、variant、any能否组合使用A可以。例如optionalvariantint, string表示要么没有值要么是 int 或 stringvariantint, any可以表达已知 int 或其类型未知的——但后者在实践中很少用因为any已经包揽了任意语义。

相关新闻