通用类型Any的思想与实现
前言:什么是Any通用类型
Any 通用类型是一种类型擦除容器,它能够存储任意类型的值,同时在需要时能够安全地恢复原始类型。
用一串代码来展示他与众不同的特性的话,那就是下面这一段:
class Test
{
public:
Test() { std::cout << "构造" << std::endl; }
Test(const Test &t) { std::cout << "拷贝" << std::endl; }
~Test()
{
std::cout << "析构" << std::endl;
}
};
int main()
{
// any库的使用
std::any a;
a = 10; // 存储 int
a = 3.14; // 存储 double
a = std::string("hello"); // 存储 string
a = Test(); // 存储自定义类型
Test *pi = std::any_cast<Test>(&a);
return 0;
}

它有着三个主要的特性:
1. 类型擦除 - 可以存储任何类型
class Any {
// 内部使用多态,隐藏具体类型
holder* _content; // 基类指针,不知道具体类型
};
// 使用者看到的是统一类型
std::vector<Any> vec; // 可以混合存储不同类型
vec.push_back(42);
vec.push_back("hello");
vec.push_back(3.14);
2. 类型安全 - 记住原始类型
Any a = 42;
int* p = a.get<int>(); // 类型匹配,安全
std::string* s = a.get<std::string>(); // ❌ 类型不匹配,返回nullptr或断言
3. 值语义 - 像普通变量一样使用
Any a = 10;
Any b = a; // 深拷贝,独立副本
Any c;
c = a; // 赋值,独立副本
Any类的由来
我们为什么要使用any类呢?它是怎么被需求出来的呢?
这是因为,在没有 any 之前,C++ 处理未知类型只能使用 void*,但是这种方式丢失了类型信息:
// 旧方式:危险且麻烦
void* data = new int(42);
// 使用者必须知道类型才能转换
int* value = (int*)data; // 如果猜错了类型就崩溃
delete data;
并且,当我们需要在同一个容器中存储不同类型的时候,就只能通过复杂的继承体系来实现。使用多态的思想才能实现:
// 没有 any:需要复杂的继承体系
class Base { virtual ~Base() {} };
class IntValue : public Base { int val; };
class StringValue : public Base { string val; };
vector<Base*> vec; // 需要手动管理内存
并且,在类似高并发服务器的场景中,我们每时每刻都在处理着大量的不知道类型的数据流:
服务器中的每⼀个Connection对连接进⾏管理,最终都不可避免需要涉及到应⽤层协议的处理,因此在Connection中需要设置协议处理的上下⽂来控制处理节奏。
但是应⽤层协议千千万,为了降低耦合度,这个协议接收解析上下⽂就不能有明显的协议倾向,它可以是任意协议的上下⽂信息,因此就需要⼀个通⽤的类型来保存各种不同的数据结构。
在C语⾔中,通⽤类型可以使⽤void* 来管理,但是在C++中,boost库和C++17给我们提供了⼀个通⽤类型any来灵活使⽤,如果考虑增加代码的移植性,尽量减少第三⽅库的依赖,则可以使⽤C++17特性中的any,或者⾃⼰来实现。
Any类的简单模拟实现:
那接下来我们就来简单的实现一下Any。
首先Any一定不是一个个模板类,否则编译的时候 Any< int> a, Any< float>b,需要传类型作为模板参数,也就是说在使⽤的时候就要确定其类型,这显然是不符合我们的要求的。
因此我们可以考虑在Any类的内部使用一个模板子类继承父类的形式来实现,即我们定义一个父类与模板子类,我们的Any可以通过持有一个父类的类成员指针,使用多态的办法来指向不同类型的子类,这个子类由于是模板,自然可以保存不同的类型。
所以我们先创建一个holder基类:
class holder
{
public:
virtual ~holder() {} // 虚析构:确保派生类正确释放资源
// 获取存储数据的类型信息
// 为什么需要这个?为了类型安全检查,防止错误的类型转换
virtual const std::type_info &type() = 0;
// 克隆当前对象
// 为什么需要clone?实现深拷贝,支持拷贝构造和赋值操作
// 因为基类指针无法直接拷贝派生类对象,需要通过多态克隆
virtual holder *clone() = 0;
};
对于这个基类holder,我们需要把它的析构函数设置为虚函数,这样我们用父类指针指向子类对象才能调用自己的析构(不管我们是否对子类的析构进行其他操作)。
另外我们需要设置两个功能,分别是一个克隆函数与一个返回自己类型的函数。
另外,我们的目的是创造一个子类来接收不同的数据类型的数据,所以我们需要使用的模板:
// placeholder模板类:真正存储数据的容器
// 为什么需要模板?因为要存储任意类型的数据
template <class T>
class placeholder : public holder // 继承holder,实现多态
{
public:
// 构造函数:保存具体数值
placeholder(const T &val) : _val(val) {}
// 返回存储数据的类型信息
// 用于运行时类型检查,确保类型安全
virtual const std::type_info &type() { return typeid(T); }
// 克隆自己:创建相同类型的新对象
// 为什么返回holder*?因为基类指针可以指向派生类对象,实现多态
virtual holder *clone() { return new placeholder(_val); }
由于我们这个子类类型主要作用是接受数据,所以直接写明一个带参的默认构造函数。
我们设置clone函数的目的是方便在Any的各种拷贝构造中,把类成员的holder指针进行一个深拷贝而不是浅拷贝。因为_content的类型是指针,所以如果只是默认构造函数,是不会对其进行处理的。在拷贝构造函数中,默认生成的拷贝构造也只会对指针变量进行一个浅拷贝而不是深拷贝。如果没有clone,你就只能这样实现:
Any(const Any &other) : _content(other._content) // 浅拷贝!
{
// 两个对象指向同一块内存
}
并且,由于我们的holder类是一个抽象基类,你是无法用new来进行深拷贝的,比如_content=new holder(other. _ content )。
这是我们必须要用clone的一个原因。
接下来就是实现Any的构造拷贝赋值重载功能:
// 默认构造
Any() : _content(nullptr) {}
template <class T>
Any(const T &val) : _content(new placeholder<T>(val)) // 模板转换构造函数,允许任意类型T构造Any
{
}
Any(const Any &other) : _content(other._content ? other._content->clone() : nullptr) {}; // 拷贝构造函数
~Any() { delete _content; }
template <class T>
T *get()
{
assert(typeid(T) == _content->type()); // 返回的数据类型,也就是想得到的数据类型必须和我们存储的数据类型是一致的
return &((placeholder<T> *)_content)->_val; // 先把_content转化为子类指针之后再指向_val
}
Any &swap(Any &other)
{
std::swap(_content, other._content); // 交换二者的父类指针
return *this; // 方便连续调用
}
template <class T>
Any &operator=(const T &val)
{
Any(val).swap(*this); // 构造一个临时对象与this交换,那么我们原先的content由于交换给了临时变量所以就被释放掉了
return *this;
}
Any &operator=(const Any &other)
{
Any(other).swap(*this);
return *this;
}
这些都放在public中就行,上面的类的定义与any的类成员变量需要放在private中。
我们实现swap的时候,返回了一个Any的引用,这是为了方便我们连续的调用,比如后面赋值重载的时候调用了:Any(val).swap(* this);。
他的意思是先通过val构造出一个临时的any类对象,让这个类对象与当前调用swap的any对象交换。
交换以后,原本类的内容就放到临时any中,出了生命域就会被析构销毁。
具体的简单实现如下:
#include <iostream>
#include <string>
#include <assert.h>
#include <unistd.h>
#include <any> //c++17的any头文件
/*
* 自定义Any类的设计思想:
* 核心问题:C++是静态类型语言,需要在编译期确定类型,但我们需要一个能存储任意类型的容器
* 解决方案:类型擦除(Type Erasure)技术
*
* 设计思路:
* 1. 定义一个非模板基类holder,提供统一接口
* 2. 定义模板子类placeholder,真正存储数据
* 3. 通过基类指针操作不同子类,实现类型擦除
*/
class Any
{
private:
// holder基类:提供统一的虚函数接口
// 为什么需要holder?因为我们需要一个统一的类型来操作所有存储的具体类型
// 但基类本身不知道具体类型,所以所有操作都通过虚函数完成
class holder
{
public:
virtual ~holder() {} // 虚析构:确保派生类正确释放资源
// 获取存储数据的类型信息
// 为什么需要这个?为了类型安全检查,防止错误的类型转换
virtual const std::type_info &type() = 0;
// 克隆当前对象
// 为什么需要clone?实现深拷贝,支持拷贝构造和赋值操作
// 因为基类指针无法直接拷贝派生类对象,需要通过多态克隆
virtual holder *clone() = 0;
};
// placeholder模板类:真正存储数据的容器
// 为什么需要模板?因为要存储任意类型的数据
template <class T>
class placeholder : public holder // 继承holder,实现多态
{
public:
// 构造函数:保存具体数值
placeholder(const T &val) : _val(val) {}
// 返回存储数据的类型信息
// 用于运行时类型检查,确保类型安全
virtual const std::type_info &type() { return typeid(T); }
// 克隆自己:创建相同类型的新对象
// 为什么返回holder*?因为基类指针可以指向派生类对象,实现多态
virtual holder *clone() { return new placeholder(_val); }
public:
T _val; // 真正存储的数据
// 为什么是public?为了方便get()函数直接访问
// 也可以设置为private并提供getter,但这里为了性能直接暴露
private:
};
holder *_content; // 指向实际存储数据的holder对象
// 这是类型擦除的关键:通过基类指针统一管理所有具体类型
public:
// 默认构造:初始化为空
// 为什么需要空状态?表示Any对象没有存储任何数据
Any() : _content(nullptr) {}
// 模板转换构造函数:允许从任意类型构造Any
// 这是Any能接收任意类型的关键
template <class T>
Any(const T &val) : _content(new placeholder<T>(val))
{
// 创建对应的placeholder对象存储数据
// new placeholder<T>(val) 在堆上分配内存
// 为什么用堆?因为不同类型大小不同,无法在栈上统一分配
}
// 拷贝构造函数:深拷贝
// 为什么需要深拷贝?因为两个Any对象应该独立拥有自己的数据
// 如果不实现深拷贝,两个对象会共享同一个_content,导致double free
Any(const Any &other) : _content(other._content ? other._content->clone() : nullptr) {}
// 析构函数:释放资源
// 为什么需要虚析构?虽然这里不是基类,但为了防止未来继承,加上更安全
~Any() { delete _content; }
// 获取存储数据的指针
// 为什么返回指针而不是引用?可以用nullptr表示类型不匹配
template <class T>
T *get()
{
// 类型安全检查:确保请求的类型与存储的类型一致
// 为什么需要这个检查?防止错误的类型转换导致未定义行为
assert(typeid(T) == _content->type());
// 关键转换:将基类指针转换为子类指针
// (placeholder<T>*)_content:强制转换为具体的placeholder类型
// ->_val:访问存储的数据
// &:返回指针,避免拷贝大对象
return &((placeholder<T> *)_content)->_val;
}
// 交换两个Any对象的内容
// 为什么需要swap?实现高效的赋值操作(copy-and-swap惯用法)
// 这种实现是异常安全的,并且可以避免重复的内存分配
Any &swap(Any &other)
{
std::swap(_content, other._content); // 只交换指针,效率高
return *this; // 返回引用支持链式调用
}
// 赋值操作符:从具体类型赋值
// 实现思路:构造临时对象 + 交换
template <class T>
Any &operator=(const T &val)
{
// 1. Any(val) 创建临时对象,存储val
// 2. swap(*this) 交换临时对象和当前对象的内容
// 3. 临时对象销毁,释放原来的_content
//
// 为什么这样实现?
// a) 异常安全:如果构造临时对象失败,当前对象保持不变
// b) 简洁:不需要手动处理资源释放
// c) 自动处理自我赋值:swap可以正确处理
Any(val).swap(*this);
return *this;
}
// 赋值操作符:从另一个Any对象赋值
Any &operator=(const Any &other)
{
// 同样的copy-and-swap惯用法
Any(other).swap(*this); // Any(other)调用拷贝构造
return *this;
}
};
/*
* 设计思想总结:
*
* 1. 类型擦除的核心机制:
* - 基类holder提供虚函数接口
* - 模板子类placeholder存储具体数据
* - 通过holder*统一管理
*
* 2. 内存管理:
* - 使用堆内存存储数据(因为类型大小不确定)
* - 通过虚析构正确释放资源
* - 深拷贝确保数据独立
*
* 3. 类型安全:
* - 存储时记录typeid
* - 获取时检查类型匹配
* - 使用assert或异常处理错误
*
* 4. 异常安全:
* - copy-and-swap惯用法保证强异常安全
* - 操作失败时对象状态不变
*
* 5. 性能考虑:
* - 小类型优化?这个实现没有,std::any有
* - 移动语义?这个实现没有,可以扩展
* - 内联优化?模板函数可以内联
*
* 与std::any的对比:
* - 功能相同:都是类型安全的任意类型容器
* - 接口差异:这个实现用get()返回指针,std::any用any_cast返回值或引用
* - 性能:std::any有小对象优化,这个实现没有
* - 完整性:std::any有emplace、reset、has_value等完整接口
*/
// 使用示例
int main() {
Any a;
a = 42; // 存储int
int* pi = a.get<int>(); // 获取int指针
std::cout << *pi << std::endl;
a = std::string("hello"); // 存储string
std::string* ps = a.get<std::string>();
std::cout << *ps << std::endl;
// a.get<double>(); // 断言失败:类型不匹配
return 0;
}
C++17的std::any
std::any 是 C++17 引入的类型安全的容器,可以存储任意类型的单个值。
#include <any>
#include <string>
#include <iostream>
int main() {
std::any a = 42; // 存储 int
a = std::string("hello"); // 存储 string
a = 3.14; // 存储 double
a = std::vector<int>{1,2,3}; // 存储 vector
}
他的使用方法也十分的简单,与我们自定义的稍微不一样的是,获取返回类型的函数接口不再是get,而是std::any_cast。
使用方法大差不差:
std::any a = 42;
// 1. 值版本(拷贝)
int i = std::any_cast<int>(a);
std::cout << i << '\n'; // 42
// 2. 引用版本(避免拷贝)
std::any b = std::string("hello");
std::string& s = std::any_cast<std::string&>(b);
s += " world";
std::cout << std::any_cast<std::string>(b) << '\n'; // "hello world"
// 3. 移动版本(转移所有权)
std::any c = std::vector<int>{1,2,3};
auto vec = std::any_cast<std::vector<int>>(std::move(c));
// c 现在是空状态
结语
通过本文的逐步剖析,我们不仅理解了 Any 通用类型的设计思想,还亲手实现了一个简化版本,并了解了 C++17 标准库中的 std::any。回顾整个探索过程,我们可以总结出以下几个关键收获:
类型擦除是 Any 的灵魂。通过“非模板基类 + 模板派生类”的经典组合,我们巧妙地绕过了 C++ 静态类型系统的限制,让一个容器能够接纳任意类型的数据。这种设计模式不仅在 Any 中应用,在 std::function、std::shared_ptr 的删除器等组件中同样发挥着重要作用。
同时,自定义实现也有其价值——它让我们清楚地看到了类型擦除的底层机制,理解了虚函数、深拷贝、copy-and-swap 惯用法等核心 C++ 技术在实战中的应用。
在实际开发中,我们应该:
- 优先使用
std::any:如果编译器支持 C++17,直接使用标准库实现 - 理解而非滥用:
Any解决了特定问题,但不要用它替代模板或std::variant - 注意性能开销:类型擦除带来便利的同时也伴随着运行时成本
Any 类的实现堪称 C++ 模板元编程和多态技术结合的典范之作。它告诉我们:即使在静态类型语言中,通过巧妙的设计,我们也能获得动态语言的灵活性,同时不失类型安全。
希望这篇文章能帮助你真正掌握 Any 的精髓——不仅是会用,更是理解其背后的设计哲学。当你下次在项目中遇到“需要存储未知类型”的需求时,相信你能够做出正确的设计决策,甚至在必要时实现自己的类型擦除容器。
记住:理解原理,方能游刃有余;掌握本质,才可举一反三。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)