
🎁std::any
std::any 是一个可以存储 任意类型**(必须是可拷贝构造的)** 单个值的容器。当你从 any 中取值时,你必须知道它的原始类型,并通过 std::any_cast 进行安全的转换。如果类型不匹配,它会抛出异常或返回空指针。
variant 类比 union,这里的 any 就类比 void*!
与原始的 void* 不同,any 会记录类型信息,并在 any_cast 时进行检查。其次它管理着自己内部存储的对象生命周期(构造、拷贝、析构),为了避免为小对象频繁分配内存,许多实现使用了小缓冲区优化(SBO,Small Buffer Optimization),MSVC 的std::string就使用了这个优化(小的存在自己的 buffer 当中,大的就去堆上申请,进行管理,好说歹说,栈上开辟空间的效率比堆上开空间的效率要高)。
这里说一下为什么存储的直接任意类型必须是可拷贝构造的:
从底层实现来看,std::any 本质是通过类型擦除 机制存储任意类型的值 ------ 它会在内部为存储的对象分配内存(小对象用栈上的 SBO 缓冲区,大对象用堆),并保存该对象的 "拷贝构造函数 / 析构函数 / 类型信息" 等函数指针;当 std::any 发生拷贝(如赋值、传参、返回)时,必须调用存储类型的拷贝构造函数,将原对象的内容复制到新的内存空间中(否则无法完成值的独立存储),同时 std::any 的 any_cast 等操作也依赖拷贝构造来生成独立的对象实例;如果类型不可拷贝(如删除了拷贝构造函数),std::any 无法完成底层的内存复制和对象生命周期管理,因此标准强制要求存储类型必须是可拷贝构造的。
cpp
#include <any>
#include <string>
struct MyData {
std::string content;
// 必须有拷贝构造函数
MyData(const MyData& other) : content(other.content) {
std::cout << "MyData拷贝构造被调用\n";
}
MyData(const char* str) : content(str) {}
};
int main() {
// 1. 存入数据(发生一次拷贝)
MyData data1 = "Hello";
std::any a = data1; // 调用MyData拷贝构造,复制到any内部
// 2. any拷贝(再发生一次拷贝)
std::any b = a; // any内部再次调用MyData拷贝构造
// 3. any_cast取出(第三次拷贝)
MyData data2 = std::any_cast<MyData>(b); // 再次调用拷贝构造
return 0;
}
这里说的直接,其实就是:
std::any 本身强制要求存储的 "值类型" 必须可拷贝构造 ,但我们可以通过指针、智能指针、std::reference_wrapper 等方式 "绕开" 这个限制 ------ 本质是把 "不可拷贝的类型" 包装成 "可拷贝的包装类型",而非直接存储不可拷贝类型本身。:
std::any 存储的直接对象必须是可拷贝构造的(这是标准强制要求,因为其底层类型擦除依赖拷贝构造完成对象的独立存储),但我们可以通过三类 "包装手段" 间接存储不可拷贝类型:
- ① 存储指针(如
T*):指针本身是可拷贝的,std::any拷贝的是指针值而非指向的对象,需手动管理对象生命周期; - ② 存储智能指针(如
std::unique_ptr<T>/std::shared_ptr<T>):智能指针本身可拷贝 / 移动,std::any拷贝的是智能指针对象(unique_ptr仅可移动),由智能指针自动管理对象生命周期; - ③ 存储
std::reference_wrapper<T>:它是引用的可拷贝包装器,std::any拷贝的是包装器而非引用的对象,需保证被引用对象生命周期长于std::any。这三种方式并非让std::any存储不可拷贝类型本身,而是存储 "可拷贝的包装类型",间接实现对不可拷贝对象的 "存储"。
std::any& 是 std::any 对象的引用类型 ,而非 std::any 内部存储的值的引用:它本身不存储任何数据,只是指向已存在的 std::any 对象
当然了,这只是说我在无聊的玩语法
为什么说 any 是类型安全的,体现在哪里:
std::any 被称为类型安全的核心在于:它并非像 void* 那样完全丢弃类型信息,而是在底层通过类型擦除机制存储了值的实际类型信息(std::type_info) ,且所有取值操作(std::any_cast)都会在运行时严格校验目标类型与存储类型是否匹配 ------ 若类型不匹配,std::any_cast<T> 会抛出 std::bad_any_cast 异常,std::any_cast<T*> 会返回 nullptr,从根本上避免了 void* 强制类型转换时可能出现的 "类型错误却无提示" 的未定义行为;
同时 std::any 会自动管理存储对象的生命周期,赋值新类型时会先析构旧值、再构造新值,不会出现内存泄漏或野指针问题,这些 "运行时类型校验 + 自动生命周期管理" 的特性,让 std::any 相比无类型校验的裸指针,实现了真正的类型安全。
刚刚说的类型擦除机制又是什么:
理解类型擦除机制,核心是抓住 "抽象基类统一接口 + 模板子类封装具体类型 + 基类指针隐藏细节 " 这三个关键点,下面用极简伪代码还原 std::any 底层的类型擦除核心逻辑:
cpp
// -------------------------- 第一步:定义统一的抽象接口(擦除具体类型) --------------------------
// 抽象基类:只声明接口,不关心具体类型,这是"擦除"的核心
struct AnyStorageBase {
// 析构接口:销毁存储的对象
virtual ~AnyStorageBase() = default;
// 拷贝接口:复制存储的对象(返回基类指针,隐藏具体类型)
virtual AnyStorageBase* clone() const = 0;
// 类型判断接口:返回存储对象的类型信息
virtual const std::type_info& get_type() const = 0;
};
// -------------------------- 第二步:模板子类封装具体类型 --------------------------
// 模板子类:为任意类型 T 实现基类接口,包裹具体类型的操作
template<class T>
struct AnyStorage : AnyStorageBase {
T value; // 真正存储具体类型的值
// 构造:接收具体类型的值,完成初始化
AnyStorage(const T& val) : value(val) {}
// 实现拷贝接口:调用 T 的拷贝构造,返回基类指针(类型擦除)
AnyStorageBase* clone() const override {
return new AnyStorage<T>(this->value); // 复制 T 类型的值
}
// 实现类型判断接口:返回 T 的类型信息
const std::type_info& get_type() const override {
return typeid(T);
}
};
// -------------------------- 第三步:封装成 std::any 对外接口 --------------------------
class Any {
private:
AnyStorageBase* storage = nullptr; // 基类指针:只认接口,不认具体类型(擦除核心)
public:
// 赋值任意类型 T:创建对应的子类对象,用基类指针指向它
template<class T>
void set_value(const T& val) {
delete storage; // 先销毁旧值
storage = new AnyStorage<T>(val); // 新值封装成子类,基类指针指向它
}
// 取值:校验类型,类型匹配则返回值,否则报错(类型安全的关键)
template<class T>
T get_value() const {
// 校验类型:对比存储的类型和目标类型
if (storage->get_type() != typeid(T)) {
throw "类型不匹配!"; // 对应 std::bad_any_cast 异常
}
// 向下转型:从基类指针转回具体子类指针(恢复类型)
AnyStorage<T>* concrete_storage = static_cast<AnyStorage<T>*>(storage);
return concrete_storage->value; // 返回 T 类型的值
}
// 拷贝 Any 对象:调用基类的 clone 接口,无需关心具体类型
Any(const Any& other) {
if (other.storage) {
this->storage = other.storage->clone(); // 只调用基类接口,擦除具体类型
}
}
~Any() { delete storage; } // 析构:调用基类析构,自动销毁子类对象
};
// -------------------------- 测试:验证类型擦除效果 --------------------------
int main() {
Any a;
// 存储 int 类型:底层创建 AnyStorage<int>,storage 指向它(基类指针)
a.set_value(10);
// 取值:校验类型为 int,返回 10
std::cout << a.get_value<int>() << std::endl;
// 存储 string 类型:销毁旧的 int 对象,创建 AnyStorage<string>
a.set_value(std::string("hello"));
// 取值:校验类型为 string,返回 "hello"
std::cout << a.get_value<std::string>() << std::endl;
// 错误示例:类型不匹配,抛出异常
// a.get_value<int>(); // 报错:类型不匹配!
return 0;
}
🔗 参考:https://en.cppreference.com/w/cpp/header/any.html
🛠️ std::any 核心接口
| 函数 | 作用 |
|---|---|
| 构造函数 | std::any any_value = 42; std::any any_value = std::string("Hello"); |
| emplace<T>(args...) | 原地构造一个类型为 T 的对象,参数 args 传递给 T 的构造函数。 |
| reset() | 销毁内部包含的对象,使 any 变为空。 |
| has_value() | 返回一个 bool,判断 any 对象当前是否包含一个值。 |
| type() | 返回一个 std::type_info const&,表示当前包含值的类型。如果 any 为空,则返回 typeid(void)。 |
std::any_cast<T> |
最重要的函数,用于从 any 对象中提取值。如果转换失败,会抛出 std::bad_any_cast 异常。 |
💻 构造和赋值
cpp
#include <any>
#include <string>
#include <iostream>
int main() {
std::any a1 = 42; // 存放 int
std::any a2 = 3.14; // 存放 double
std::any a3 = std::string("Hello"); // 存放 std::string
std::any a4;
a4 = std::pair<std::string, std::string>("xxxx", "yyyy");
// 使用 emplace 原地构造
a3.emplace<std::string>("World"); // 现在 a3 包含 "World",之前的 "Hello" 被销毁
std::cout << sizeof(a1) << '\n';
std::cout << sizeof(a4) << '\n';
// 检查是否有值
if (a3.has_value()) {
std::cout << "a3 has value" << std::endl;
}
// 获取类型信息
const std::type_info& ti = a1.type();
std::cout << "a1 type: " << ti.name() << std::endl;
}
这段代码中体现了 std::any 两种核心构造方式:
- 一是直接赋值构造 (隐式构造),比如
std::any a1 = 42;、a2 = 3.14;、a3 = std::string("Hello");,这种方式会调用std::any的模板赋值运算符,底层通过类型擦除机制,调用存储类型(int/double/std::string)的拷贝构造函数,将值复制到std::any管理的内存中(小对象用栈上 SBO 缓冲区,大对象用堆); - 二是默认构造 + 后赋值 ,比如
std::any a4;先创建空的std::any对象(无存储值,has_value()返回false),再通过a4 = std::pair<...>完成赋值,此时会先初始化空对象的内部存储,再拷贝构造std::pair对象; - 三是emplace 原地构造 ,代码中 a3.emplacestd::string("World"); 是更高效的方式,它会先销毁 a3 中原有的 "Hello" 字符串(调用 std::string 析构函数),再在 std::any 内部的内存空间直接构造 "World" 字符串,避免了 "先构造临时字符串再拷贝" 的额外开销,是原地创建值的最优方式。
std::any 的赋值核心遵循 "先销毁旧值,再拷贝 / 构造新值" 的规则:比如 a3 最初存储 "Hello",执行 emplace<std::string>("World") 时,底层会先调用原 std::string 的析构函数释放内存,再调用 std::string("World") 的构造函数在原有内存空间创建新值;
而 a4 = std::pair<...> 这种普通赋值,因为 a4 初始为空,所以直接调用 std::pair 的拷贝构造函数,将 std::pair("xxxx", "yyyy") 复制到 a4 的内存中;
需要注意的是,无论哪种赋值,std::any 都会记录新值的类型信息(比如 int/std::pair),并更新内部的类型擦除相关函数指针(拷贝、析构等),保证后续 type()、any_cast 能正确识别类型。
代码中 sizeof(a1) 和 sizeof(a4) 输出相同(通常为指针大小 + 类型信息存储大小,比如 8/16 字节),这体现了 std::any 的内存特性:无论存储的是 int(4 字节)还是 std::pair(大对象),std::any 对象本身的大小固定 ------ 小对象(如 int/double/ 短字符串)存储在 std::any 内部的 SBO 缓冲区(栈),大对象(如 std::pair、长字符串)存储在堆上,std::any 仅持有指向堆内存的指针,因此对象本身大小不变;同时,构造 / 赋值后可通过 has_value() 检查是否有有效存储值(如 a3.has_value() 返回 true),通过 type() 获取存储值的类型信息(如 a1.type() 返回 int 的 type_info),这些都是构造 / 赋值后类型擦除机制保留的关键信息,保证了类型安全。
🔍 any_cast 三种取值方式
🔗 参考:https://en.cppreference.com/w/cpp/utility/any/any_cast.html
cpp
#include <iostream>
#include <string>
#include <vector>
#include <cassert>
// vector 中存储 any 类型
void anyVector() {
std::string str = "hello world";
std::vector<std::any> v = {1, 1.2, str};
for (const auto& item : v) {
if (item.type() == typeid(int)) {
std::cout << "整数配置:" << std::any_cast<int>(item) << '\n';
}
else if (item.type() == typeid(double)) {
std::cout << "浮点数配置:" << std::any_cast<double>(item) << '\n';
}
else if (item.type() == typeid(std::string)) {
std::cout << "字符串配置:" << std::any_cast<std::string&>(item) << '\n';
}
else {
assert(false);
}
}
}
int main() {
std::any a1 = 42; // 存放 int
std::any a2 = 3.14; // 存放 double
std::any a3 = std::string("Hello"); // 存放 std::string
// 方式一:转换为值的类型(如果类型不匹配,抛出 std::bad_any_cast)
try {
int int_value = std::any_cast<int>(a1); // 正确,a1 存放的是 int
std::cout << "value: " << int_value << '\n';
double double_value = std::any_cast<double>(a1); // 错误!抛出异常
}
catch (const std::bad_any_cast& e) {
std::cout << "Cast failed: " << e.what() << '\n';
}
// 方式二:转换为值的引用(存储对象的引用,要尽量避免这样使用)
// 这里 a3 存放的返回的是 a3 存储对象的引用
std::string str_ref1 = std::any_cast<std::string>(a3);
str_ref1[0]++;
std::cout << std::any_cast<std::string>(a3) << '\n';
std::string str_ref2 = std::any_cast<std::string&>(a3);
str_ref2[0]++;
std::cout << std::any_cast<std::string&>(a3) << '\n';
std::string&& str_ref3 = std::any_cast<std::string&&>(std::move(a3));
str_ref3[0]++;
std::cout << std::any_cast<std::string&>(a3) << '\n';
std::string str_ref4 = std::any_cast<std::string&&>(std::move(a3));
str_ref4[0]++;
std::cout << std::any_cast<std::string&>(a3) << '\n';
// 方式三:转换为指针(如果类型不匹配,返回 nullptr,不会抛出异常)
if (auto ptr = std::any_cast<int>(&a1)) { // 传递指针,返回指针
std::cout << "Value via pointer: " << *ptr << '\n';
}
else {
std::cout << "Not an int or is empty.\n";
}
anyVector();
}
C++ 标准中 std::any_cast 主要有 3 个重载版本,对应三种取值方式:
cpp
// 方式1:转换为值类型(值拷贝)
template <class T>
T any_cast(const any& operand);
template <class T>
T any_cast(any& operand);
template <class T>
T any_cast(any&& operand); // 这个可不是万能引用,没有引用折叠,又没有自动推导 auto 就是一个右值形参
// 方式2:转换为引用类型(本质是上述模板的自然推导,核心重载如下)
template <class T>
T& any_cast(any& operand);
template <class T>
const T& any_cast(const any& operand);
template <class T>
T&& any_cast(any&& operand);
// 方式3:转换为指针类型
template <class T>
T* any_cast(any* operand) noexcept;
template <class T>
const T* any_cast(const any* operand) noexcept;
std::any_cast 取值方式 1:转换为值类型(值拷贝)
这种方式是直接从 std::any 中拷贝出存储值的副本,代码中 int int_value = std::any_cast<int>(a1); 就是典型示例 ------std::any_cast<T>(any_obj) 会先校验 any_obj 存储的类型是否为 T,若匹配则调用 T 的拷贝构造函数,生成一个独立的 T 类型值;
若类型不匹配(如 std::any_cast<double>(a1)),会直接抛出 std::bad_any_cast 异常,这是类型安全的核心体现。该方式的特点是取值后得到的是 "副本 ",修改副本不会影响 std::any 内部存储的原值(如 str_ref1 修改后,a3 中的字符串仍为 "Hello"),但对于大类型(如长字符串、大容器)会有拷贝开销。
std::any_cast 取值方式 2:转换为引用类型(直接操作原值)
这种方式通过 std::any_cast<T&>(any_obj) 或 std::any_cast<T&&>(std::move(any_obj)) 获取 std::any 内部存储值的引用 ,而非副本:代码中 std::string& str_ref2 = std::any_cast<std::string&>(a3) 拿到的是 a3 中字符串的左值引用,修改 str_ref2 会直接改变 a3 内部的原值("Hello" 变为 "Jello");
而 std::string&& str_ref3 = std::any_cast<std::string&&>(std::move(a3)) 是获取右值引用,可转移 std::any 内部值的所有权,修改右值引用也会直接改变原值,但需注意右值引用使用后,原 std::any 对象的状态会变为 "有效但未定义"(如 str_ref4 拷贝后,a3 内部字符串已被转移,再访问可能出现空值 / 乱码)。该方式的核心是 "无拷贝、直接操作原值",适合对性能要求高的场景,但需谨慎避免悬垂引用。
std::any_cast 取值方式 3:转换为指针类型(安全校验 + 无异常)
这种方式是通过指针间接访问 std::any 内部的值,代码中 auto ptr = std::any_cast<int>(&a1) 是关键示例 ------std::any_cast<T>(&any_obj) 会校验 any_obj 存储的类型是否为 T,若匹配则返回指向内部值的 T* 指针;若类型不匹配或 any_obj 为空,不会抛出异常,而是直接返回 nullptr。 该方式的优势是 "非侵入式" 且无异常风险,适合需要优雅处理类型不匹配的场景(如代码中通过 if (ptr) 判断是否为 int 类型),通过指针解引用(*ptr)可直接操作 std::any 内部的原值,兼顾了安全性和灵活性。
🧩 综合运用样例
实现一个简单哈希桶,桶长度大于 8 存到红黑树,桶长度小于等于 8 存到链表,我们可以直接将上面的 variant 版本哈希桶出来尝试改成 any 版本。
cpp
#include <any>
#include <cassert>
#include <list>
#include <set>
#include <vector>
template<class K, size_t Len = 8>
class HashTable {
public:
HashTable(size_t len) : _tables(len, std::list<K>()) {}
void insert(const K& key) {
size_t hash = key % _tables.size();
// 链表插入
auto listInsert = [&key](std::list<K>& lt) -> bool {
// 小于,则插入到链表
if (lt.size() < Len) {
lt.push_back(key);
return true;
}
else {
// 大于,则转换到红黑树
std::set<K> s(lt.begin(), lt.end());
s.insert(key);
return false;
}
};
// 红黑树插入
auto setInsert = [&key](std::set<K>& s) {
s.insert(key);
};
if (auto ptr = std::any_cast<std::list<K>>(&_tables[hash])) {
if (!listInsert(*ptr)) {
std::set<K> s(ptr->begin(), ptr->end());
s.insert(key);
_tables[hash] = std::move(s);
}
}
else if (auto ptr = std::any_cast<std::set<K>>(&_tables[hash])) {
setInsert(*ptr);
}
else {
assert(false);
}
}
bool find(const K& key) {
size_t hash = key % _tables.size();
if (!_tables[hash].has_value()) {
return false;
}
// 链表查找
auto listFind = [&key](std::list<K>& lt) -> bool {
return std::find(lt.begin(), lt.end(), key) != lt.end();
};
// 红黑树查找
auto setFind = [&key](std::set<K>& s) -> bool {
return s.count(key);
};
if (auto ptr = std::any_cast<std::list<K>>(&_tables[hash])) {
return listFind(*ptr);
}
else if (auto ptr = std::any_cast<std::set<K>>(&_tables[hash])) {
return setFind(*ptr);
}
else {
assert(false);
return false;
}
}
private:
std::vector<std::any> _tables;
};
int main() {
HashTable<int, 8> ht(10);
for (int i = 0; i < 10; ++i) {
ht.insert(i * 10);
}
std::cout << ht.find(15) << std::endl;
std::cout << ht.find(30) << std::endl;
return 0;
}
这个哈希表案例是 std::any 典型的 "动态类型存储" 应用场景:std::any 被用来定义哈希桶的存储类型,让每个桶既能存储 std::list<K>(链表)也能存储 std::set<K>(红黑树)------ 初始化时桶默认存储链表,插入元素时通过 std::any_cast 指针方式(安全且不抛异常)判断当前桶的类型,若链表元素数超过阈值则将链表转为红黑树并通过 std::any 的赋值(std::move 转移所有权)完成类型替换,查找元素时同样通过 std::any_cast 识别桶的实际类型(链表 / 红黑树),调用对应的查找逻辑;std::any 在这里的核心价值是用类型安全的方式实现 "单一存储位置动态切换不同容器类型" ,无需手动维护类型标记,且通过 any_cast 指针方式避免了异常风险,既满足了 "元素少用链表省空间、元素多用红黑树提效率" 的性能平衡需求,又相比传统 void* 或联合体保证了类型安全和自动内存管理。
🧐 std::any vs std::variant(面试必备)
从功能角度,他们都是用于存储多种不同类型的类型安全的单值容器,使用方法上也有诸多相似。
使用方式 :std::any 存储任意类型,使用时必须通过 std::any_cast 转换访问;std::variant 存储预定义的有限类型,通过 std::get 或 std::visit 访问。
安全性 :std::variant 在编译期就知道所有可能的类型,访问更安全(跳转表的原因,编译的时候就生成了);std::any 类型信息只有运行时才知道,转换失败会抛出异常或返回空指针。
性能 :std::variant 是栈分配,性能更高;std::any 对于大对象会在堆上分配内存,且每次访问都需要类型检查,有一定开销。
适用场景:
- 当你明确知道所有可能的类型时,优先使用
std::variant; - 当你需要存储完全未知的任意类型时,才使用
std::any。