
【C 面试真题】聊聊 C 的 std::any 与 std::variant“一个变量运行起来才知道是什么类型”——C17 给了两条路any开集装得下任何类型和variant闭集备选类型列得出来。背得出any 是类型擦除、variant 是类型安全 union只是及格真考你的是any_cast 失败会怎样、visit 怎么分发、overload 惯用法怎么手写、开集闭集怎么选。本文对比讲解这两兄弟。一、开场装不同类型的两条路❓ 介绍一下 any 和 variant✅ 都是[C17]的多类型容器方向一开一合工具类型集合底层访问方式any开集任意类型类型擦除any_cast猜一个variantA, B, …闭集备选列表类型安全 unionvisit 分发全覆盖any a42;astring(hi);// 换成什么都行variantint,stringv;v42;// 只能是 int 或 stringvhis;回答思路先一句话定位——开集用 any、闭集用 variant——再各报核心操作any_cast / visit面试官就会往深处的原理和选型追问。二、any类型擦除的盒子❓ any 是怎么工作的✅类型擦除——盒子不把类型写进签名运行期才知道里面是什么any a42;astring(hi);// 取值any_cast类型必须精确匹配string sany_caststring(a);// ✅ 匹配成功// double d // any_castdouble(a);// ❌ 抛 bad_any_castany_castT要求精确匹配int 就是 intdouble 不行失败抛bad_any_cast探测式访问用指针形式any_castT*(a)——失败返回nullptr 不抛查询当前类型a.type()返回type_info可与typeid(T)比较。适用场景很窄但真实异构容器vectorany装不同类型、跨接口传未知负载、对接脚本/配置系统。⚠️误用信号如果取值处的 any_cast 类型是穷举的一小撮——你要的其实是 variant下节闭集用 variant开集才 any。三、variant类型安全的 union❓ variant 比 union 安全在哪✅记得当前装的是谁——union 读错类型直接未定义行为variant 有全套防护variantint,stringvhis;// 问现在是谁holds_alternativestring(v);v.index();// 1第几个备选// 取get 错类型抛异常stringsgetstring(v);// getint(v); // ❌ bad_variant_access// 探测get_if 返回指针不抛if(autopget_ifint(v)){// *p 是 int——这里不会进}赋值会自动析构旧值、构造新值——从 string 换成 intstring 的内存自动释放这正是 union 手工做不到的。emplace还能按位置/类型原地构造并转发参数。⚠️穷尽性检查是 variant 的工程杀手锏新增一个备选类型所有没覆盖它的 visit立刻编译失败——any 的问题要等运行期才炸。四、visitvariant 的灵魂搭档❓ visit 是干什么的✅ 把当前是谁分发给对应版本的访问器——编译期生成分发表运行期按 index 一次索引variantint,stringv42;visit([](autox){coutx;// x 类型随 v 推导},v);overload 惯用法高频手写题——多个 lambda 各认领一个类型// [C17] 继承 using 展开 推导指引templateclass...Tsstructoverload:Ts...{usingTs::operator()...;};templateclass...Tsoverload(Ts...)-overloadTs...;visit(overload{[](inti){/* 整型分支 */},[](string s){/* 字符串分支 */},},v);原理每个 lambda 是一个类overload继承全部using Ts::operator()...把各家的调用运算符引入作用域构成重载集合——visit 按当前类型选中最匹配的。能默写出这三件套说明你真用过。⚠️ 访问器少写一个类型的分支编译直接报错——这就是穷尽性备选多、多数处理相同时加一个泛型 lambda[](auto)兜底。五、对比与选型一张表收束❓ any 和 variant 到底怎么选✅ 一个问题定案备选类型在编译期列得出来吗维度variantany类型集合编译期闭集运行期开集访问visit 分发全覆盖any_cast猜一个漏处理某类型编译期报错运行期异常大小编译期定内联存储小对象优化大了堆分配典型场景状态机、消息、AST异构容器、外部数据再和继承多态对比第三个多类型方案三选一法则种类封闭、要值语义 → variant种类开放、要运行期任意 → any种类要跨模块开放扩展、行为依附对象身份 → 虚函数继承。variant 零堆分配可拷贝继承开放但有虚调用——按集合开闭 值/对象语义两个维度切。六、实现细节与坑❓ 两者的底层和坑有哪些✅ 三个细节① 大小sizeof(variantA,B) 最大成员 对齐 index 标志编译期定死、零堆分配any 内置小缓冲小对象优化装不下才堆分配sizeof(any)随实现几十字节。② variant 的首个备选要能默认构造——variantFoo,int v;默认构造 FooFoo 无默认构造就得用monostate空类型占位打头。③ valueless_by_exception——variant 赋值时新值构造抛异常会进入罕见的无值状态强异常保证的妥协valueless_by_exception()可查询。面试当冷知识讲很加分。⚠️ variant 备选类型不能是引用、数组、void——想装引用用reference_wrapperT。七、面试高频追问❓ Q1any_cast 失败的两种形式✅ 引用形式any_castT(a)失败抛bad_any_cast指针形式any_castT*(a)失败返回 nullptr 不抛——想安静探测类型用指针形式。❓ Q2visit 的分发表是运行期 if 链吗✅ 不是。编译期把每个备选类型 × 访问器实例化成表数组运行期按 active index 一次索引取函数——O(1) 分发且类型覆盖检查发生在编译期。多 variant 版本visit(f, v1, v2)枚举所有组合备选一多实例爆炸要克制。❓ Q3get 和 get_if 怎么选✅ 确信当前类型、错了就是程序 bug →getT抛异常炸出来不确定、探测式分支 →get_ifT(v)返回 nullptr 安静处理。和 any_cast 的引用/指针两形式是同一个设计思想。❓ Q4monostate 是干什么的✅ 空类型占位符——给没有默认构造的类型打头阵或表达空状态这个备选项本身。❓ Q5什么时候用 variant、什么时候用继承✅种类封闭、行为随类型变化、想要值语义→ variant visit种类开放、跨模块扩展、行为依附对象身份→ 虚函数继承。前者可拷贝、无堆、穷尽检查后者开放扩展、经典 OOP。❓ Q6any 的小对象优化是什么✅ any 内部有一个内联缓冲装得下类型小且对齐合适就直接放缓冲、零堆分配装不下才堆分配。所以小类型 any 的拷贝很快、大类型有分配成本——sizeof(any)固定内容去哪由实现决定。八、总结速查表考点一句话结论分界开集 any / 闭集 variantany 取值any_cast 精确匹配any 探测any_castT* 返回 nullptrvariant 本质记得当前是谁的 union判断/取值holds_alternative / get 抛 / get_if 空分发visit overloadoverload 三件套继承 using 展开 推导指引穷尽性少个分支编译报错vs 继承值语义闭集 vs 开放扩展首备选要默认构造否则 monostate一句话回顾开集用 any、闭集用 variantany 靠类型擦除、any_cast精确匹配指针形式探测不抛variant 是记得当前是谁的 union取值 get/get_if、分发 visit、手写overload 三件套继承 using 展开 推导指引穷尽性检查让它新增类型时编译期报错——而继承多态是第三条路开放扩展、对象语义。如果您觉得本篇内容对你有帮助欢迎点赞 、收藏 ⭐、转发 。下期我们继续标准库篇——聊聊chrono时间库duration、时钟、时间点怎么配合为什么计时要用 steady_clock敬请关注