26、C++17 核心特性:std::any,存储任意类型的值
说实话,刚看到 std::any 的时候,我第一反应是:这不就是 void* 的现代版吗?
后来在项目中真正用起来,才发现自己太天真了。std::any 比 void* 安全得多,也聪明得多。它知道自己在存什么类型,而且不会让你随随便便就踩内存越界的坑。
什么是 std::any?
简单说,std::any 是一个可以存储任意类型值的容器。你放一个 int 进去,它存 int;放一个 string 进去,它存 string;放一个自定义的类对象进去,它也能存。
它不像模板那样在编译期就确定类型,而是运行时动态决定的。嗯,这有点像 JavaScript 的变量,但 C++ 毕竟是静态类型语言,所以它加了一层类型安全的保护。
核心要点:std::any 是 C++17 引入的类型擦除工具,用于存储任意可拷贝构造的类型。
基本用法
先看个最简单的例子:
#include <any>
#include <iostream>
#include <string>
int main() {
std::any a = 42; // 存 int
std::any b = std::string("hello"); // 存 string
std::any c = 3.14; // 存 double
// 读取时需要用 any_cast
std::cout << std::any_cast<int>(a) << std::endl;
std::cout << std::any_cast<std::string>(b) << std::endl;
std::cout << std::any_cast<double>(c) << std::endl;
return 0;
}
这里要注意,any_cast 如果类型不匹配,会抛出 std::bad_any_cast 异常。我曾经在项目里忘了检查类型,结果线上崩了一次,排查了半天才发现是类型写错了。
避坑指南:使用 any_cast 前,最好先用 a.type() == typeid(int) 判断一下当前存储的类型,或者用 any_cast<int>(&a) 返回指针的方式,这样失败时返回 nullptr 而不是抛异常。
类型安全与检查
为什么说 std::any 比 void* 安全?因为它记住了类型信息。
void* 只是一个裸指针,你存进去一个 int,取出来时如果强转成 double,程序不会报错,但数据完全错乱了。std::any 会在运行时检查类型,类型不对就抛异常或者返回空指针。
std::any a = 42;
// 安全的方式:用指针形式
if (auto* p = std::any_cast<int>(&a)) {
std::cout << *p << std::endl;
} else {
std::cout << "类型不匹配" << std::endl;
}
// 不安全的方式:直接 cast,类型不对就抛异常
try {
std::string s = std::any_cast<std::string>(a);
} catch (const std::bad_any_cast& e) {
std::cerr << "捕获异常: " << e.what() << std::endl;
}
我个人习惯用指针形式的 any_cast,因为不用写 try-catch,代码更干净。你想想看,如果每个 any_cast 都要包一层 try,那代码得多难看。
修改与重置
std::any 支持重新赋值和清空:
std::any a = 100;
a = std::string("新的值"); // 重新赋值,类型可以变
a.reset(); // 清空,此时 a.has_value() 返回 false
if (!a.has_value()) {
std::cout << "any 现在是空的" << std::endl;
}
这里有个细节:reset() 之后,之前存储的对象会被析构。如果你存的是一个大型对象,及时 reset 可以释放内存。我在处理网络消息时,经常用 any 暂存不同类型的消息体,处理完就 reset,避免内存堆积。
性能考量
天下没有免费的午餐。std::any 虽然方便,但有一定的性能开销:
- 堆分配:对于小型类型(如 int、double),std::any 可能做小对象优化(类似 std::string 的 SSO),直接在栈上存储。但对于较大的类型,会触发堆分配。
- 类型擦除:每次存取都需要运行时类型检查,比直接使用具体类型慢一些。
- 拷贝开销:std::any 要求存储的类型是可拷贝构造的,每次拷贝 any 对象时,内部存储的对象也会被拷贝。
| 操作 | 相对开销 | 说明 |
|---|---|---|
| 构造(小对象) | 低 | 可能使用小对象优化,无堆分配 |
| 构造(大对象) | 中 | 触发堆分配和拷贝 |
| any_cast(指针形式) | 低 | 仅类型检查,无拷贝 |
| any_cast(值形式) | 中 | 类型检查 + 拷贝构造 |
| 拷贝 any 对象 | 中到高 | 取决于内部存储的对象大小 |
说白了,如果你在性能敏感的热路径上频繁使用 any,那可能不太合适。但如果是配置解析、消息分发这类场景,any 的便利性远大于那点性能损失。
实际应用场景
我在项目中主要用 std::any 做两件事:
- 属性系统:比如一个游戏引擎中,每个实体可以附加任意属性。用 any 存储属性值,比用 void* + 类型标记安全得多。
- 消息总线:不同模块之间传递消息,消息体可以是任意类型。用 any 做类型擦除,接收方自己决定怎么解析。
// 一个简单的消息总线示例
class MessageBus {
std::unordered_map<std::string, std::any> messages;
public:
template<typename T>
void publish(const std::string& key, T&& value) {
messages[key] = std::forward<T>(value);
}
template<typename T>
T* subscribe(const std::string& key) {
auto it = messages.find(key);
if (it != messages.end()) {
return std::any_cast<T>(&it->second);
}
return nullptr;
}
};
这个模式我在好几个项目里都用过,配合模板,用起来很顺手。不过要注意,消息的发送方和接收方必须约定好类型,否则 any_cast 会失败。
与 std::variant 的区别
很多人会把 std::any 和 std::variant 搞混。我简单说一下区别:
- std::variant:编译期就确定可以存储哪些类型(类似 union 的现代版),类型列表是固定的。
- std::any:运行时可以存储任意类型,类型不固定。
如果你知道可能存储的类型集合(比如要么是 int,要么是 string,要么是 double),用 variant 更高效。如果你完全不知道会存什么类型(比如从配置文件里读出来的值),那就用 any。
我的建议:能用 variant 就用 variant,实在不行再上 any。variant 的性能更好,而且类型安全是在编译期保证的,比运行时的 any_cast 更可靠。
知识结构图
下面这张图帮你理清 std::any 的核心概念和关系:
总结
std::any 是 C++17 里一个很实用的工具。它不像模板那样死板,也不像 void* 那样危险。说白了,它就是在「类型安全」和「灵活性」之间找到了一个不错的平衡点。
我个人觉得,如果你在写一些需要运行时多态的代码,但又不想引入虚函数继承体系,any 是个很好的选择。当然,别滥用,该用 variant 的时候还是用 variant。
最后提醒一句:any_cast 之前一定要检查类型,或者用指针形式。这个坑我踩过,希望你别踩。
一句话记住:std::any 是类型安全的 void*,但别把它当万能药,该用 variant 时用 variant。