前言
今天继续推进仿 muduo 的 C++ 高并发服务器项目。
前面更多是在学习 Reactor、定时器、HTTP 请求解析这些模块。今天补的是一个看起来不直接处理网络 IO,但在网络库中非常实用的小组件:Any 通用容器。
在服务器开发中,有时我们希望给一个连接绑定一些用户自定义数据。比如:
- 登录后的用户信息
- 当前连接所属的业务会话
- 请求处理过程中临时保存的上下文
- 后续回调函数需要携带的自定义参数
这些数据的类型并不固定。如果直接把类型写死,网络库的通用性就会变差。所以 muduo 这类网络库通常会提供一个通用上下文对象,让使用者自己决定里面保存什么类型的数据。
今天我先通过 std::any 理解这种"一个对象保存任意类型数据"的思想,然后自己实现了一个简化版 Any,重点理解类型擦除、模板多态、深拷贝和类型安全取值。
一、先看标准库 std::any 的使用
C++17 标准库里提供了 std::any,它可以保存任意可拷贝类型的数据。
测试代码如下:
std::any a;
a = 10;
int *pi = std::any_cast<int>(&a);
std::cout << *pi << std::endl;
a = std::string("hello");
std::string *ps = std::any_cast<std::string>(&a);
std::cout << *ps << std::endl;
这里同一个变量 a,一开始保存的是 int,后面又保存了 std::string。
运行结果为:
10
hello
通过这个小实验可以看到,std::any 的使用方式非常直接:
- 赋值时可以保存不同类型的数据
- 取值时需要通过
std::any_cast<T>指定原始类型 - 取值类型必须和保存类型一致,否则就会失败
这里也顺便总结一下 C++17 中 std::any 的基本用法。
首先,使用 std::any 需要包含头文件:
#include <any>
并且编译时需要使用 C++17 标准,例如:
g++ any.cpp -std=c++17
在我的测试用例中:
std::any a;
a = 10;
这两行表示先定义一个空的 std::any 对象,然后让它保存一个 int 类型的数据。取值时不能直接强转,而是要使用:
int *pi = std::any_cast<int>(&a);
这里传入的是 &a,所以 std::any_cast<int>(&a) 返回的是一个 int*。如果类型正确,就能得到有效指针;如果类型不匹配,就会返回空指针。
接着我又写了:
a = std::string("hello");
std::string *ps = std::any_cast<std::string>(&a);
这说明同一个 std::any 对象可以重新保存另外一种类型的数据。原来保存的 int 会被替换掉,现在 a 里面保存的是 std::string。
所以 std::any 的使用可以概括成三步:
- 定义一个
std::any对象 - 像普通变量一样给它赋值
- 用
std::any_cast<原始类型>把数据取出来
它和普通变量最大的区别是:普通变量的类型在定义时就固定了,而 std::any 可以在运行过程中保存不同类型的数据。不过取值时一定要记住原来保存的真实类型。
这正是我后面想自己实现的能力。
二、自己实现一个简化版 Any
我实现的 Any 代码如下:
#include <cassert>
#include <iostream>
#include <string>
#include <typeinfo>
#include <utility>
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(NULL) {}
template<class T>
Any(const T &val) : _content(new placeholder<T>(val)) {}
Any(const Any &other)
: _content(other._content ? other._content->clone() : NULL) {}
~Any() { delete _content; }
Any &swap(Any &other) {
std::swap(_content, other._content);
return *this;
}
template<class T>
T *get() {
assert(_content != NULL);
assert(typeid(T) == _content->type());
return &((placeholder<T>*)_content)->_val;
}
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;
}
};
这段代码的核心思想是:外层 Any 不直接知道自己保存的具体类型,而是通过一个基类指针 _content 指向真正保存数据的子类对象。
三、holder:统一的抽象基类
holder 是一个抽象基类:
class holder {
public:
virtual ~holder() {}
virtual const std::type_info& type() = 0;
virtual holder *clone() = 0;
};
它本身不保存数据,只提供两个统一接口:
type():返回当前保存数据的真实类型clone():克隆出一个新的对象,用于支持拷贝构造
因为 Any 里面只保存了一个 holder*,所以不管真实数据是 int、std::string,还是自定义类对象,外层都可以用同一种方式管理。
这就是类型擦除的第一步:把不同类型的数据对象统一隐藏到一个基类指针后面。
四、placeholder:真正保存数据的模板子类
真正保存数据的是 placeholder<T>:
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;
};
这里的 T 就是实际保存的数据类型。
比如:
Any a = 10;
内部创建的其实是:
new placeholder<int>(10);
如果写成:
Any a = std::string("hello");
内部创建的就是:
new placeholder<std::string>(std::string("hello"));
也就是说,Any 表面上看起来是一个通用容器,但真正保存数据时,仍然是通过模板生成对应类型的子类。
五、_content:用基类指针管理任意类型
Any 内部只有一个成员变量:
holder *_content;
这个指针可以指向不同的子类对象:
Any
|
|_content ----> placeholder<int>
Any
|
|_content ----> placeholder<std::string>
Any
|
|_content ----> placeholder<Test>
这样做的好处是,Any 本身不需要知道具体类型,只要通过多态调用 type() 和 clone() 就可以了。
这也是实现通用容器的关键。
六、get:按照原始类型取出数据
取值函数如下:
template<class T>
T *get() {
assert(_content != NULL);
assert(typeid(T) == _content->type());
return &((placeholder<T>*)_content)->_val;
}
这里有两个检查:
assert(_content != NULL);
表示当前 Any 里面必须已经保存了数据。
assert(typeid(T) == _content->type());
表示取值类型必须和保存类型完全一致。
比如原来保存的是:
a = 10;
那么取值时必须写:
int *pa = a.get<int>();
如果错误地写成:
std::string *ps = a.get<std::string>();
就会触发断言。
这一点和 std::any_cast 的思想一样:Any 虽然可以保存任意类型,但取出来的时候必须指定正确类型。
七、clone:为什么需要深拷贝
拷贝构造函数如下:
Any(const Any &other)
: _content(other._content ? other._content->clone() : NULL) {}
这里不能简单地让两个 Any 对象保存同一个 _content 指针。否则两个对象析构时都会 delete 同一块内存,导致重复释放。
所以我在基类中设计了一个虚函数:
virtual holder *clone() = 0;
每个具体子类自己实现克隆逻辑:
virtual holder *clone() { return new placeholder(_val); }
这样,拷贝一个 Any 对象时,会重新创建一份内部数据。
这就是深拷贝。
八、赋值运算符:临时对象 + 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;
}
这里用的是一个很巧妙的方式:先用新数据构造一个临时 Any 对象,再和当前对象交换 _content 指针。
比如:
a = std::string("nihao");
执行过程可以理解为:
- 先构造一个临时
Any,里面保存std::string("nihao") - 临时对象和当前对象交换
_content - 当前对象获得新数据
- 临时对象析构时释放旧数据
这样既完成了新数据替换,也避免了手动处理旧资源释放的复杂逻辑。
九、测试用例说明
为了验证 Any 的功能,我分别测试了标准库 std::any 和自己实现的 Any。
测试 1:使用 std::any 保存整型
测试代码:
std::any a;
a = 10;
int *pi = std::any_cast<int>(&a);
std::cout << *pi << std::endl;
预期输出:
10
这个测试说明 std::any 可以保存并取出 int 类型数据。
测试 2:使用 std::any 保存字符串
测试代码:
a = std::string("hello");
std::string *ps = std::any_cast<std::string>(&a);
std::cout << *ps << std::endl;
预期输出:
hello
这个测试说明同一个 std::any 对象可以从保存 int 切换为保存 std::string。
测试 3:使用自定义 Any 保存 Test 对象
我定义了一个 Test 类,用构造、拷贝构造和析构函数观察对象生命周期:
class Test {
public:
Test() { std::cout << "构造" << std::endl; }
Test(const Test &t) { std::cout << "拷贝" << std::endl; }
~Test() { std::cout << "析构" << std::endl; }
};
测试代码:
Any a;
{
Test t;
a = t;
}
预期现象:
构造
拷贝
析构
其中:
- 创建局部对象
t时输出"构造" - 执行
a = t时,placeholder<Test>内部保存一份副本,所以输出"拷贝" - 离开代码块后,局部对象
t析构,所以输出"析构"
这说明 Any 里面保存的是对象副本,而不是局部变量 t 的地址。
等到 Any a 自己析构时,它内部保存的那份 Test 副本也会被释放,所以还会再输出一次"析构"。
测试 4:使用自定义 Any 保存 int
测试代码:
a = 10;
int *pa = a.get<int>();
std::cout << *pa << std::endl;
预期输出:
10
这个测试说明自定义 Any 的模板赋值和 get<int>() 可以正常工作。
测试 5:使用自定义 Any 保存 std::string
测试代码:
a = std::string("nihao");
std::string *ps = a.get<std::string>();
std::cout << *ps << std::endl;
预期输出:
nihao
这个测试说明同一个 Any 对象可以替换原来的 int 数据,保存新的字符串数据。
同时,替换数据时旧的 int 对象会随着临时对象析构被释放。
测试 6:类型错误测试
错误测试代码:
a = 10;
std::string *ps = a.get<std::string>();
预期现象:
触发 assert(typeid(T) == _content->type())
因为当前 Any 里面保存的是 int,却想按照 std::string 取出,这种行为是不允许的。
所以 Any 虽然可以保存任意类型,但并不意味着可以随便强转。保存什么类型,取出时就必须使用什么类型。
十、完整测试代码
#include <any>
#include <cassert>
#include <iostream>
#include <string>
#include <typeinfo>
#include <unistd.h>
#include <utility>
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(NULL) {}
template<class T>
Any(const T &val) : _content(new placeholder<T>(val)) {}
Any(const Any &other)
: _content(other._content ? other._content->clone() : NULL) {}
~Any() { delete _content; }
Any &swap(Any &other) {
std::swap(_content, other._content);
return *this;
}
template<class T>
T *get() {
assert(_content != NULL);
assert(typeid(T) == _content->type());
return &((placeholder<T>*)_content)->_val;
}
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;
}
};
class Test {
public:
Test() { std::cout << "构造" << std::endl; }
Test(const Test &t) { std::cout << "拷贝" << std::endl; }
~Test() { std::cout << "析构" << std::endl; }
};
int main()
{
std::any stdAny;
stdAny = 10;
int *pi = std::any_cast<int>(&stdAny);
std::cout << *pi << std::endl;
stdAny = std::string("hello");
std::string *stdPs = std::any_cast<std::string>(&stdAny);
std::cout << *stdPs << std::endl;
Any a;
{
Test t;
a = t;
}
a = 10;
int *pa = a.get<int>();
std::cout << *pa << std::endl;
a = std::string("nihao");
std::string *ps = a.get<std::string>();
std::cout << *ps << std::endl;
while (1) sleep(1);
return 0;
}
这里最后的:
while (1) sleep(1);
只是为了让测试程序临时保活,方便观察进程状态,不属于 Any 的核心逻辑。实际写成项目组件时可以去掉。
十一、和 muduo 项目的关系
这个 Any 组件后续可以用在连接上下文中。
比如一个 TcpConnection 对象可以提供类似这样的接口:
void setContext(const Any &context);
Any *getContext();
这样网络库本身不需要关心业务层到底保存什么数据。
业务层可以保存:
conn->setContext(UserInfo(...));
也可以保存:
conn->setContext(std::string("login session"));
网络库只负责保存和转交,具体类型由业务层自己决定。
这样设计可以让底层网络库保持通用,而业务层又能灵活扩展自己的连接状态。
总结
今天主要实现了一个简化版 Any 通用容器,并通过多个测试用例验证了它的基本能力。
这篇内容的重点不是把 Any 写成完整工业级组件,而是理解它背后的几个关键思想:
- 通过基类指针和模板子类实现类型擦除
- 通过
typeid记录并检查真实类型 - 通过
clone()支持深拷贝 - 通过临时对象和
swap完成安全赋值 - 通过测试用例观察对象保存、替换、拷贝和释放过程
在仿 muduo 项目中,Any 后续可以作为连接上下文的基础组件,为 TcpConnection 保存用户自定义状态。
今天这个模块虽然不直接处理 epoll 或 socket,但它能让网络库的接口更加灵活,也让项目逐渐从"能跑"走向"更像一个可复用的网络库"。