C++万能类:any

std::any 是 C++17 引入的一个极其重要的特性,它为 C++ 这种强类型语言带来了类似动态语言(如 Python 变量)的灵活性,同时保持了类型安全。

简单来说,std::any****是一个类型安全的容器,它可以存储"任意"类型的单个值

以下是对 std::any 的详细讲解,分为用法实现原理 、以及与其他技术的对比三个部分。


一、 std::any 的核心用法

在 C++17 之前,如果我们想在一个变量里存不同类型的数据,通常只能用 void*(不安全,不仅丢失类型信息,还无法自动释放内存)或者 union(极其局限)。std::any 解决了这些问题。

1. 基础操作:存储与赋值

你可以将任何**可拷贝构造(Copy Constructible)**的类型赋值给 std::any

复制代码
#include <iostream>
#include <any>
#include <string>
#include <vector>

int main() {
    // 1. 默认构造(为空)
    std::any a;

    // 2. 存储 int
    a = 10; 

    // 3. 存储 double (原本的 int 被销毁,类型变为 double)
    a = 3.14;

    // 4. 存储 std::string
    a = std::string("Hello World");

    // 5. 存储复杂对象
    a = std::vector<int>{1, 2, 3};

    return 0;
}
2. 访问数据:std::any_cast

这是 std::any 最关键的地方。由于 std::any 内部擦除了类型信息,编译器不知道里面存的是什么。取值时,你必须显式告诉它你要取什么类型。

  • 值/引用转换(抛出异常): 如果类型不对,会抛出 std::bad_any_cast

  • 指针转换(不抛异常): 如果传入的是指针,类型不对时返回 nullptr

    #include <iostream>
    #include <any>

    int main() {
    std::any a = 100;

    复制代码
      try {
          // 【正确】类型匹配
          int val = std::any_cast<int>(a); 
          std::cout << "Value: " << val << std::endl;
    
          // 【错误】类型不匹配(虽然 100 是数字,但在 any 里它是 int,不是 float)
          // 这行会抛出 std::bad_any_cast
          float f = std::any_cast<float>(a); 
    
      } catch(const std::bad_any_cast& e) {
          std::cout << "Error: " << e.what() << std::endl;
      }
    
      // 【安全访问模式】使用指针
      // 如果 a 中存储的不是 int,这里 p 将是 nullptr,不会崩也不会抛异常
      if (int* p = std::any_cast<int>(&a)) {
          std::cout << "Pointer access: " << *p << std::endl;
      } else {
          std::cout << "a does not contain an int" << std::endl;
      }

    }

3. 状态查询与重置
复制代码
std::any a = 10;

// 检查是否有值
if (a.has_value()) {
    // ...
}

// 获取类型信息 (type_info)
if (a.type() == typeid(int)) {
    std::cout << "It's an integer!" << std::endl;
}

// 清空/重置
a.reset(); // 此时 has_value() 为 false

二、 std::any 的实现原理(深度解析)

很多同学会好奇:为什么 C++ 这种静态类型语言,能够在运行时随便换类型?

其核心技术被称为 Type Erasure(类型擦除)

1. 核心架构:基类与模板子类

std::any 的内部通常不直接存储值,而是持有一个指针,指向一个堆上(或栈上)的对象。为了能让这个指针指向任意类型,它利用了多态

我们可以尝试手写一个简化版的 Any 来理解:

复制代码
class MyAny {
private:
    // 1. 定义一个抽象基类(接口)
    struct StorageBase {
    virtual ~StorageBase() {}
    virtual std::unique_ptr<StorageBase> clone() const = 0; // 用于拷贝 any
    virtual const std::type_info& getType() const = 0;      // 用于类型检查
};
    
    // 2. 定义一个模板子类,用于存储具体的类型 T
    template<typename T>
    struct StorageImpl : StorageBase {
    T value; // 这里存具体的值
    
    StorageImpl(T v) : value(v) {}
    
    // 实现虚函数
    std::unique_ptr<StorageBase> clone() const override {
        return std::make_unique<StorageImpl<T>>(value);
    }
    
    const std::type_info& getType() const override {
        return typeid(T);
    }
};
    
    // 3. 成员变量:基类指针
    std::unique_ptr<StorageBase> storage;

public:
    // 构造函数:接受任意类型
    template<typename T>
    MyAny(T v) : storage(std::make_unique<StorageImpl<T>>(v)) {}
    
    // ... 省略拷贝构造、赋值等 ...
    
    // 获取类型信息
    const std::type_info& type() const {
        if (storage) return storage->getType();
        return typeid(void);
    }
    
    // 友元函数用于 cast
    template<typename T>
    friend T* my_any_cast(MyAny* any);
};

逻辑解析:

  1. 当我们执行 MyAny a = 10; 时,编译器推导出 Tint
  2. 它实例化 StorageImpl<int>,并将 10 存入其中的 value
  3. MyAny 内部持有 StorageBase* 指向这个 StorageImpl<int> 对象。
  4. 类型擦除 :在 MyAny 这一层,它只知道自己持有 StorageBase,不知道具体是 int 还是 string。只有在运行时调用虚函数(如 getType)或者强转回 StorageImpl<int> 时,才能恢复类型信息。
2. 性能优化:SBO (Small Buffer Optimization)

上述的简单实现有一个大问题:每次赋值都要 new****内存 。如果我只存一个 intbool,每次都在堆上分配内存,性能太差了。

工业级(STL)的实现通常引入了 SBO(小缓冲优化)

  • 内部联合体std::any 内部通常有一个 union,包含一个 void* 指针(用于大对象)和一个小的字节数组(比如 16 字节或 32 字节)。
  • 判断大小
    • 如果存的对象很小(如 int, double),直接存入内部字节数组,无需堆内存分配
    • 如果存的对象很大(如 std::vector),才在堆上分配,并将指针存入。

这意味着,对于基础数据类型,std::any 的性能是非常高效的。


三、 思考:std::any vs void* vs std::variant

为了更好地理解逻辑性问题,我们需要对比相似技术:

|----------|--------------------|-----------------------------------|--------------------|
| 特性 | std::any | std::variant (C++17) | void* |
| 类型限制 | 无限制(只要能拷贝) | 编译期确定的有限集合 (如 int OR string ) | 无限制 |
| 类型安全 | 安全 (运行时检查,抛异常) | 安全 (编译期/运行时检查) | 不安全 (完全靠程序员自觉) |
| 内存管理 | 自动 (RAII) | 自动 (栈上分配) | 手动 (容易内存泄漏) |
| 存储位置 | 可能在堆,也可能在栈 (SBO) | 只在栈上 (大小等于最大成员的大小) | 指向哪里就是哪里 |
| 性能 | 中等 (可能有虚函数/动态分配开销) | 极高 (无动态分配) | 高 (裸指针) |
| 使用场景 | 类型完全不可知,且开放 | 类型是已知的几种之一 | 与 C 语言接口交互 |

什么时候用 std::any
  • 当你在设计一个通用的事件系统、消息总线、或者属性配置系统时。
  • 你不知道用户会传什么类型进来,可能是 int,也可能是用户自定义的 MyClass
  • 例子:Qt 的 QVariant 机制本质上就是 std::any 的变种,用于 GUI 控件存储任意用户数据。
什么时候用 std::variant
  • 如果你的逻辑很明确:"这个变量要么是数字,要么是字符串,绝对不会是别的"。
  • 此时用 std::variant<int, string> 更好,因为它不需要动态分配内存,且编译器能帮你检查是否处理了所有类型。

四、 总结与建议

  1. std::any****是现代 C++ 的"万能胶囊":利用类型擦除技术,允许在这个容器里装入任何东西。
  2. 安全性 :虽然它像动态类型,但它是类型安全的,必须通过 any_cast 显式还原类型,否则报错。
  3. 实现核心模板子类继承非模板基类 + SBO 小对象优化
相关推荐
梦帮科技16 小时前
第三十四篇:开源社区运营:GitHub Stars增长策略
开发语言·前端·爬虫·python·docker·架构·html
智者知已应修善业16 小时前
【数组删除重复数据灵活算法可修改保留重复数量】2024-3-4
c语言·c++·经验分享·笔记·算法
Cappi卡比17 小时前
【无标题】
c++
小成2023032026517 小时前
Linux高级
linux·开发语言
m0_5027249517 小时前
QT - 系统托盘
开发语言·qt
汉克老师17 小时前
GESP2025年12月认证C++五级真题与解析(编程题2 (相等序列))
c++·算法·贪心算法·中位数·质数分解
还不秃顶的计科生17 小时前
python循环中的enumerate用法
开发语言·python
资生算法程序员_畅想家_剑魔17 小时前
Java常见技术分享-26-事务安全-锁机制-常见的锁实现
java·开发语言
坚持学习前端日记17 小时前
桌面端与移动端JS桥技术对比及跨平台实现
开发语言·javascript·harmonyos