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 做两件事:

  1. 属性系统:比如一个游戏引擎中,每个实体可以附加任意属性。用 any 存储属性值,比用 void* + 类型标记安全得多。
  2. 消息总线:不同模块之间传递消息,消息体可以是任意类型。用 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 基本操作 构造:std::any a = 42; 读取:any_cast<T>(a) 修改:a = 新值 重置:a.reset() 类型安全 运行时类型检查 指针形式:返回 nullptr 值形式:抛异常 type() 获取 type_info 性能考量 小对象优化(SSO) 大对象堆分配 拷贝开销 类型检查开销 应用场景 属性系统 消息总线 配置解析

总结

std::any 是 C++17 里一个很实用的工具。它不像模板那样死板,也不像 void* 那样危险。说白了,它就是在「类型安全」和「灵活性」之间找到了一个不错的平衡点。

我个人觉得,如果你在写一些需要运行时多态的代码,但又不想引入虚函数继承体系,any 是个很好的选择。当然,别滥用,该用 variant 的时候还是用 variant。

最后提醒一句:any_cast 之前一定要检查类型,或者用指针形式。这个坑我踩过,希望你别踩。

一句话记住:std::any 是类型安全的 void*,但别把它当万能药,该用 variant 时用 variant。

专注资料整理