最折磨人的,不是一上线就炸的 bug。
是“偶尔炸一次”的 bug。
同一段代码,在 A 机器能跑,在 B 机器崩。
你翻日志,常常只剩一句:类型不对。
当年大家怎么硬扛
把时间拨回去。
那会儿还没有 std::any。
要传“任意值”,大家常用 void* 加口头约定。
写入的人说“我放的是字符串”。
读取的人说“我猜你放的是整数”。
然后线上就变成抽奖机。
后来的解法先剧透
C++17 给了一个盒子:std::any。
它做的事很朴素:值和真实类型一起存。
读取时必须类型匹配。
不匹配就明确失败,不让脏数据蒙混过关。
它具体怎么兜住这个坑?
看完下面两个小场景,你就会秒懂。
一条时间线:std::any 是怎么长出来的
2000 年,Kevlin Henney 在文章里提出了 any 这个思路。
核心目标很直接:别再用 void* 裸奔,要“能装任意值,但取值要检查类型”。
2001 年,Boost.Any 发布。
它把这个思路做成可用库,社区用了很多年。
C++ 程序员第一次有了“type-safe void*(类型安全版 void*)”这个说法。
2013 年,WG21 提案 N3804 明确写了:提案基于 Boost.Any。
提案里把接口、异常语义、any_cast 细节都逐步打磨。
例如指针版 any_cast 失败返回 nullptr,这点特意对齐了 dynamic_cast 的使用习惯。
2016 年,提案 P0220 把 Library Fundamentals TS 里的 any 并入 C++17 主标准。
你会看到它从 std::experimental::any 进入 std::any。
这一步的意义是“从试验品变成正式语法工具”。
2017 年 12 月,C++17 发布。
<any> 成为标准头文件。
到这一步,这条十几年的演化链才算闭环。
它借鉴了哪些来源(以及各自怎么处理)
先看 C 世界。
void* 的做法是“全靠约定”,语言本身不帮你验类型。
优点是灵活,缺点是错了直接未定义行为。
再看 Boost.Any。
它把“值”和“真实类型”绑在同一个盒子里,读取用 any_cast。
类型不匹配时,值版本抛异常,指针版本给空指针。
然后看 Java 的 Object。
它也允许“先存成通用父类型,再强转回来”,转错会抛 ClassCastException。
理念很像:把“错”从静态阶段挪到运行时,但至少是可见的错,不是静默坏数据。
Go 的 interface{}(现在常写 any)也一样。
你做类型断言 x.(T),失败会 panic;用 v, ok := x.(T) 则是安全分支。
这和 C++ 里“值版 cast 抛异常,指针版 cast 给空”是同一类权衡。
Rust 的 Any 走得更偏“显式检查”。
它常用 downcast_ref::<T>() -> Option<&T>,失败就是 None。
你几乎被迫先判空,再用值。
std::any 的位置就在中间。
比 void* 安全得多,比 variant 更灵活。
代价是:你要在读取点承担运行时类型判断。
新名词当场掰开
“类型擦除”不是“删掉类型”。
它是把具体类型藏到统一接口后面,只在需要时再做检查。
你可以把它理解成“前台只给一个取号机,后台仍然知道每个人是谁”。
“装箱”是把具体值塞进统一盒子。
比如把 int、std::string 都装进 std::any。
“拆箱”是按期望类型把值拿出来。
“运行期检查”是程序跑起来后再验类型。
它比编译期晚,但比 void* 靠猜强太多。
你至少能得到明确失败信号。
先把老坑复现:void* 为什么危险
最直觉的办法是 void*。
它只保存地址,不保存“地址里到底是什么类型”。
void* p = new std::string("8080");
int port = *static_cast<int*>(p); // 坑在这行
这段代码能编译。
但语义已经错了:你塞的是 std::string,却按 int 去读。
结果叫未定义行为,可能崩,可能看起来“暂时没事”。
再看 std::any 怎么把坑变浅
故事很常见。
配置中心里同一个 key,在某次发布后从字符串变成了整数。
业务代码还按老类型去取,结果只在部分机器报错。
#include <any>
#include <string>
std::any v = std::string("8080");
int port = std::any_cast<int>(v); // 抛 bad_any_cast
any_cast<int>(v) 的意思是:“请按 int 取出”。
但盒子里真实住着 std::string,所以它拒绝配合。
它不会偷偷给你垃圾值,而是直接报错:bad_any_cast。
any_cast 两种拿法:一眼看懂
any_cast 还有指针版本。
类型匹配时返回有效指针;不匹配时返回 nullptr(空指针,可以理解成“这里没有你要的类型”)。
这样你可以自己决定兜底策略。
if (auto s = std::any_cast<std::string>(&v)) {
int port = std::stoi(*s);
}
这段写法的重点是“先验证,再解引用”。
流程很像 C 里先判空再用指针。
你把不可控崩溃,换成了可控分支。
横向看一眼:不同语言都在填同一个坑
你会发现这不是 C++ 一家之痛。
void* 是“最自由”的那类方案,自由到错误也自由,错了以后连报错方式都不稳定。
后来 tag + union 想加一层保险,但标签和数据分开维护,团队一忙就容易脱节。
std::variant 走的是另一条路:先把类型集合写死,编译器替你看门。
std::any 则是把门放到运行时,取值那一刻再核验身份证。
Java 的 Object 强转失败会抛 ClassCastException,Go 的类型断言要么 panic 要么 ok=false,Rust 的 Any 下转失败给 None,本质上都在做同一件事:把“猜类型”改成“验类型”。
放到工程里看:它到底什么时候顺手
先看配置系统。
键很多,值有 int、有 string、有自定义对象,结构天然是“散”的。
这时候 std::any 很省心,容器统一,检查集中在读取点。
std::unordered_map<std::string, std::any> cfg;
cfg["port"] = 8080;
cfg["host"] = std::string("127.0.0.1");
if (auto p = std::any_cast<int>(&cfg["port"])) {
start_server(*p);
}
再看插件参数。
插件是外部团队写的,你很难要求所有人一步到位地对齐类型。
入口做一次收口校验,比把风险撒到业务深处靠谱得多。
using Args = std::unordered_map<std::string, std::any>;
int get_timeout(const Args& args, int def) {
if (auto p = std::any_cast<int>(&args.at("timeout_ms"))) return *p;
return def;
}
还有事件总线。
同一条通道里跑不同消息体,用 any 往往比拉一套复杂继承层次更实用。
但契约要写清楚,不然只是把老坑换了个壳。
struct Event {
std::string topic;
std::any payload;
};
void on_event(const Event& e) {
if (e.topic == "connected") {
if (auto id = std::any_cast<int>(&e.payload)) {
std::cout << "peer=" << *id << "\n";
}
}
}
如果类型集合本来就固定,比如你非常确定只有 int 和 string,那 variant 往往更稳。
编译器能在你漏分支时直接拉警报,这比线上日志温柔得多。
using Port = std::variant<int, std::string>;
int to_port(const Port& v) {
if (auto p = std::get_if<int>(&v)) return *p;
return std::stoi(std::get<std::string>(v));
}
还有一个常被忽略的点:热路径。
any 在高频循环里可能带来额外分配和检查成本,核心计算路径里要谨慎。
std::vector<std::any> xs;
for (int i = 0; i < n; ++i) xs.push_back(i);
int sum = 0;
for (auto& x : xs) sum += std::any_cast<int>(x);
说到底怎么选
“编译期”就是程序还没跑、编译器在挑错的时候。
“运行期”是程序已经上线,用户已经在点按钮的时候。
std::any 把一部分检查从前者移到了后者,换来的是开放输入场景下的灵活。
所以它不是“更高级的万能类型”,而是边界层的缓冲器。
你真不知道会来什么类型,用 any,但要在入口显式校验。
你已经知道类型集合而且短期稳定,就别犹豫,用 variant 或明确结构体。
一句话记忆:void* 靠猜,variant 靠穷举,any 靠运行时核验。
参考来源(原始资料)
- WG21 N3804(Any Library Proposal,2013):https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3804.html
- WG21 P0220R0(把 Library Fundamentals 组件并入 C++17,2016):https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0220r0.html
- Boost.Any 文档(可追溯到 2001):https://www.boost.org/doc/libs/release/doc/html/any.html
- cppreference:
std::any_cast行为说明:https://en.cppreference.com/w/cpp/utility/any/any_cast - cppreference:C++17 特性来源说明:https://en.cppreference.com/w/cpp/17
- Oracle Java 文档:
ClassCastException:https://docs.oracle.com/javase/8/docs/api/java/lang/ClassCastException.html - Go 语言规范:类型断言:https://go.dev/ref/spec
- Rust 标准库:
std::any::Any:https://doc.rust-lang.org/std/any/trait.Any.html