仿muduo库项目铺垫2!
感谢佬们支持!
一、为什么需要 Any?
在实现一个高并发网络服务器时,每一个 Connection 对象负责管理一条连接,而连接不可避免地要涉及应用层协议的处理。
为了控制处理节奏,Connection 中需要保存一个协议处理上下文。
问题来了:应用层协议千千万万,HTTP、WebSocket、自定义协议......每种协议的上下文数据结构都不一样。如果把协议类型写死在 Connection 里,耦合度极高,换个协议就得改底层代码。
理想的设计是: 上下文可以是任意类型,Connection 不关心里面存的是什么,只管存和取。
在 C 语言中,这个问题可以用 void* 解决,但类型信息完全丢失,不安全。
Boost 库和 C++17 标准库为我们提供了 any 来解决这个问题。但如果项目需要兼容 C++11/14,或者希望减少第三方库依赖,就需要自己实现一个 Any 类。
二、如何设计 Any?
2.1 首先想到模板
最直接的思路是用模板:
template<class T>
class Any {
T _val;
};
但这样有个致命问题:使用时必须显式指定类型。
Any<int> a = 10; // 必须写<int>
Any<string> b = "hi"; // 必须写<string>
这又不方便,看着也不优雅
2.2 类型擦除:嵌套父子类结构
解决思路是类型擦除(Type Erasure),核心思想如下:
- 定义一个非模板基类
holder - 定义一个模板子类
placeholder<T>,用来存储真正的数据 Any类只持有一个父类指针holder*
这样,Any 类本身不是模板,使用时无需指定类型,而真正的数据类型信息被"藏"进了子类里。
Any
└── holder* _content ← 父类指针,类型固定
↑
placeholder<int> ← 子类,存 int
placeholder<string> ← 子类,存 string
placeholder<MyStruct> ← 子类,存任意类型
每次赋值,都在堆上 new 一个对应类型的 placeholder<T> 对象,旧的对象被析构释放。对外只暴露父类指针,外部感知不到类型变化,这就是类型擦除。
三、完整代码实现
我们先造一个在外部的父类,其中都有虚函数
cpp
// 非模板基类,用于类型擦除
class holder {
public:
virtual ~holder() {}
virtual const std::type_info& type() = 0;//返回类型信息
virtual holder* clone() = 0;
};
子类继承父类
cpp
template<class T>
class placeholder: public holder
{
public:
placeholder(const T &val): _val(val) {}
// 获取子类对象保存的数据类型
virtual const std::type_info& type() { return typeid(T); }
// 针对当前的对象自身,克隆出一个新的子类对象
virtual holder *clone()
{
return new placeholder(_val);
}
public:
T _val;
};
整个any类我们这样设计
cpp
holder *_content;
public:
Any():_content(NULL) {}
template<class T>
Any(const T &val)
{}
Any(const Any &other)
{}
~Any()
{}
Any &swap(Any &other)
{
}
// 返回子类对象保存的数据的指针
template<class T>
T *get()
{
}
//赋值运算符的重载函数:
template<class T>
Any& operator=(const T &val)
{
}
Any& operator=(const Any &other)
{
}
实现!
#include <iostream>
#include <typeinfo>
#include <cassert>
class Any {
private:
// 非模板基类,用于类型擦除
class holder {
public:
virtual ~holder() {}
virtual const std::type_info& type() = 0;
virtual holder* clone() = 0;
};
// 模板子类,真正存储数据
template<class T>
class placeholder : public holder {
public:
placeholder(const T& val) : _val(val) {}
virtual const std::type_info& type() {
return typeid(T);
}
virtual holder* clone() {
return new placeholder(_val);
}
public:
T _val;
};
holder* _content; // 父类指针,对外隐藏真实类型
public:
Any() : _content(nullptr) {}
template<class T>
Any(const T& val) : _content(new placeholder<T>(val)) {}
// 拷贝构造:克隆一份新的子类对象
Any(const Any& other) : _content(other._content ? other._content->clone() : nullptr) {}
~Any() { delete _content; }
Any& swap(Any& other) {
std::swap(_content, other._content);
return *this;
}
// 取出数据:将父类指针转为子类指针,再取成员
template<class T>
T* get() {
assert(typeid(T) == _content->type());
return &((placeholder<T>*)_content)->_val;
}
// 赋值重载:构造临时对象后 swap,旧数据随临时对象析构
template<class T>
Any& operator=(const T& val) {
Any(val).swap(*this);
return *this;
}
Any& operator=(const Any& other) {
Any(other).swap(*this);
return *this;
}
};
四、关键细节解析
4.1 为什么赋值用 swap 而不是直接赋值?
template<class T>
Any& operator=(const T& val) {
Any(val).swap(*this); // 构造临时对象,交换指针,临时对象析构时释放旧数据
return *this;
}
这是 C++ 中经典的"copy-and-swap"惯用法:
- 用新值构造一个临时
Any对象(此时新数据在临时对象里) - 把临时对象的
_content和this->_content交换 - 临时对象析构,自动释放旧数据
好处是异常安全 ,新对象构造失败时,this 的数据完好无损。
4.2 拷贝构造为什么要 clone?
Any(const Any& other) : _content(other._content ? other._content->clone() : nullptr) {}
不能直接复制指针,否则两个 Any 对象指向同一块内存,析构时会 delete 两次导致崩溃。
clone() 是纯虚函数,子类实现时 new 一个新的 placeholder<T> 对象并返回,实现深拷贝。
4.3 get() 的类型萃取
template<class T>
T* get() {
assert(typeid(T) == _content->type());
return &((placeholder<T>*)_content)->_val;
}
从外部看,get<int>() 就像是"把 int 类型从 Any 中萃取出来",有点类似 C++ 模板元编程中**类型萃取(type traits)**的感觉。
区别在于:类型萃取是编译期行为,而 get() 是运行期通过 typeid 做类型校验,类型不匹配会触发 assert,保证安全。
五、测试验证
class Test {
public:
Test() { std::cout << "构造" << std::endl; }
Test(const Test& t) { std::cout << "拷贝" << std::endl; }
~Test() { std::cout << "析构" << std::endl; }
};
int main() {
Any a;
a = 10;
int* pa = a.get<int>();
std::cout << *pa << std::endl; // 输出 10
a = std::string("nihao");
std::string* ps = a.get<std::string>();
std::cout << *ps << std::endl; // 输出 nihao
{
Test t; // 构造
a = t; // 拷贝(构造 placeholder<Test>)
} // t 析构
// a 出作用域,placeholder<Test> 析构 → Test 析构
return 0;
}
输出:
10
nihao
构造
拷贝
析构
析构
注意到两次析构:一次是 t 出作用域,一次是 a 持有的那份拷贝在 a 析构时释放,符合预期。
六、与 C++17 std::any 的对比
C++17 标准库提供了 std::any,底层原理与本文实现基本一致,也是类型擦除。
#include <any>
std::any a = 10;
std::any b = std::string("hello");
int val = std::any_cast<int>(a); // 取出 int
std::string s = std::any_cast<std::string>(b); // 取出 string
std::any_cast 对应本文的 get<T>(),类型不匹配时抛出 std::bad_any_cast 异常(比 assert 更优雅)。
如果项目支持 C++17,优先使用标准库的 std::any;需要兼容低版本或避免依赖时,本文的手动实现完全可以胜任。
七、总结
| 对比项 | void* | Any(本文实现) | std::any(C++17) |
|---|---|---|---|
| 类型安全 | ❌ | ✅ assert 校验 | ✅ 异常校验 |
| 需要显式类型 | 取出时需要 | 取出时需要 | 取出时需要 |
| C++11 兼容 | ✅ | ✅ | ❌ |
| 实现复杂度 | 低 | 中 | 无需实现 |
手动实现 Any 的核心思路只有一个:用非模板基类指针持有模板子类对象,实现类型擦除 。理解了这个,再去看 std::function、std::any 的源码,会发现异曲同工。
总结
做总结,本篇博客介绍了通用类型any及其模拟实现!
水平有限,还请各位大佬指正。如果觉得对你有帮助的话,还请三连关注一波。希望大家都能拿到心仪的offer哦。
