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 小对象优化
相关推荐
chushiyunen几秒前
python中的@Property和@Setter
java·开发语言·python
小樱花的樱花7 分钟前
C++ new和delete用法详解
linux·开发语言·c++
froginwe118 分钟前
C 运算符
开发语言
fengfuyao98538 分钟前
低数据极限下模型预测控制的非线性动力学的稀疏识别 MATLAB实现
开发语言·matlab
摇滚侠1 小时前
搭建前端开发环境 安装 nodejs 设置淘宝镜像 最简化最标准版本 不使用 NVM NVM 高版本无法安装低版本 nodejs
java·开发语言·node.js
t198751281 小时前
MATLAB十字路口车辆通行情况模拟系统
开发语言·matlab
yyk的萌1 小时前
AI 应用开发工程师基础学习计划
开发语言·python·学习·ai·lua
Amumu121382 小时前
Js:正则表达式(一)
开发语言·javascript·正则表达式
努力的章鱼bro2 小时前
操作系统-FileSystem
c++·操作系统·risc-v·filesystem
96772 小时前
cURL curl
c++