【C++ 面试真题】23. 聊聊 C++ 的 std::any 与 std::variant
2026/8/20 20:31:20 网站建设 项目流程

【C++ 面试真题】聊聊 C++ 的 std::any 与 std::variant

“一个变量,运行起来才知道是什么类型”——C++17 给了两条路:any(开集,装得下任何类型)和variant(闭集,备选类型列得出来)。背得出"any 是类型擦除、variant 是类型安全 union"只是及格,真考你的是"any_cast 失败会怎样、visit 怎么分发、overload 惯用法怎么手写、开集闭集怎么选"。本文对比讲解这两兄弟。


一、开场:装"不同类型"的两条路

❓ 介绍一下 any 和 variant?

✅ 都是[C++17]的"多类型容器",方向一开一合:

工具类型集合底层访问方式
any开集:任意类型类型擦除any_cast(猜一个)
variant<A, B, …>闭集:备选列表类型安全 unionvisit 分发(全覆盖)
any a=42;a=string("hi");// 换成什么都行variant<int,string>v;v=42;// 只能是 int 或 stringv="hi"s;

回答思路:先一句话定位——开集用 any、闭集用 variant——再各报核心操作(any_cast / visit),面试官就会往深处的原理和选型追问。


二、any:类型擦除的盒子

❓ any 是怎么工作的?

类型擦除——盒子不把类型写进签名,运行期才知道里面是什么:

any a=42;a=string("hi");// 取值:any_cast,类型必须精确匹配string s=any_cast<string>(a);// ✅ 匹配成功// double d =// any_cast<double>(a);// ❌ 抛 bad_any_cast
  • any_cast<T>要求精确匹配(int 就是 int,double 不行),失败抛bad_any_cast
  • 探测式访问用指针形式any_cast<T*>(&a)——失败返回nullptr 不抛
  • 查询当前类型:a.type()返回type_info,可与typeid(T)比较。

适用场景很窄但真实:异构容器(vector<any>装不同类型)、跨接口传"未知负载"、对接脚本/配置系统。

⚠️误用信号:如果取值处的 any_cast 类型是穷举的一小撮——你要的其实是 variant(下节):闭集用 variant,开集才 any。


三、variant:类型安全的 union

❓ variant 比 union 安全在哪?

记得当前装的是谁——union 读错类型直接未定义行为,variant 有全套防护:

variant<int,string>v="hi"s;// 问:现在是谁?holds_alternative<string>(v);v.index();// 1(第几个备选)// 取:get 错类型抛异常string&s=get<string>(v);// get<int>(v); // ❌ bad_variant_access// 探测:get_if 返回指针,不抛if(autop=get_if<int>(&v)){// *p 是 int——这里不会进}

赋值会自动析构旧值、构造新值——从 string 换成 int,string 的内存自动释放,这正是 union 手工做不到的。emplace还能按位置/类型原地构造并转发参数。

⚠️穷尽性检查是 variant 的工程杀手锏:新增一个备选类型,所有没覆盖它的 visit立刻编译失败——any 的问题要等运行期才炸。


四、visit:variant 的灵魂搭档

❓ visit 是干什么的?

✅ 把"当前是谁"分发给对应版本的访问器——编译期生成分发表,运行期按 index 一次索引:

variant<int,string>v=42;visit([](auto&&x){cout<<x;// x 类型随 v 推导},v);

overload 惯用法(高频手写题)——多个 lambda 各认领一个类型:

// [C++17] 继承 + using 展开 + 推导指引template<class...Ts>structoverload:Ts...{usingTs::operator()...;};template<class...Ts>overload(Ts...)->overload<Ts...>;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(variant<A,B>)= 最大成员 + 对齐 + index 标志,编译期定死、零堆分配;any 内置小缓冲(小对象优化),装不下才堆分配,sizeof(any)随实现几十字节。

② variant 的首个备选要能默认构造——variant<Foo,int> v;默认构造 Foo;Foo 无默认构造就得用monostate(空类型占位)打头。

③ valueless_by_exception——variant 赋值时新值构造抛异常,会进入罕见的"无值"状态(强异常保证的妥协),valueless_by_exception()可查询。面试当冷知识讲很加分。

⚠️ variant 备选类型不能是引用、数组、void——想装引用用reference_wrapper<T>


七、面试高频追问

❓ Q1:any_cast 失败的两种形式?

✅ 引用形式any_cast<T&>(a)失败抛bad_any_cast;指针形式any_cast<T*>(&a)失败返回 nullptr 不抛——想安静探测类型用指针形式。

❓ Q2:visit 的分发表是运行期 if 链吗?

✅ 不是。编译期把"每个备选类型 × 访问器"实例化成表(数组),运行期按 active index 一次索引取函数——O(1) 分发,且类型覆盖检查发生在编译期。多 variant 版本visit(f, v1, v2)枚举所有组合,备选一多实例爆炸,要克制。

❓ Q3:get 和 get_if 怎么选?

✅ 确信当前类型、错了就是程序 bug →get<T>(抛异常炸出来);不确定、探测式分支 →get_if<T>(&v)(返回 nullptr 安静处理)。和 any_cast 的引用/指针两形式是同一个设计思想。

❓ Q4:monostate 是干什么的?

✅ 空类型占位符——给"没有默认构造的类型"打头阵,或表达"空状态"这个备选项本身。

❓ Q5:什么时候用 variant、什么时候用继承?

种类封闭、行为随类型变化、想要值语义→ variant + visit;种类开放、跨模块扩展、行为依附对象身份→ 虚函数继承。前者可拷贝、无堆、穷尽检查;后者开放扩展、经典 OOP。

❓ Q6:any 的小对象优化是什么?

✅ any 内部有一个内联缓冲,装得下(类型小且对齐合适)就直接放缓冲、零堆分配;装不下才堆分配。所以小类型 any 的拷贝很快、大类型有分配成本——sizeof(any)固定,内容去哪由实现决定。


八、总结速查表

考点一句话结论
分界开集 any / 闭集 variant
any 取值any_cast 精确匹配
any 探测any_cast<T*> 返回 nullptr
variant 本质记得当前是谁的 union
判断/取值holds_alternative / get 抛 / get_if 空
分发visit + overload
overload 三件套继承 + using 展开 + 推导指引
穷尽性少个分支编译报错
vs 继承值语义闭集 vs 开放扩展
首备选要默认构造,否则 monostate

一句话回顾

开集用 any、闭集用 variant:any 靠类型擦除、any_cast精确匹配(指针形式探测不抛);variant 是记得当前是谁的 union,取值 get/get_if、分发 visit、手写overload 三件套(继承 + using 展开 + 推导指引);穷尽性检查让它新增类型时编译期报错——而继承多态是第三条路:开放扩展、对象语义。

如果您觉得本篇内容对你有帮助,欢迎点赞 👍、收藏 ⭐、转发 📢。下期我们继续标准库篇——聊聊<chrono>时间库:duration、时钟、时间点怎么配合,为什么计时要用 steady_clock,敬请关注 👋

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询