仿 muduo 高并发服务器项目:实现 Any 通用容器并理解 std::any

前言

今天继续推进仿 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 的使用可以概括成三步:

  1. 定义一个 std::any 对象
  2. 像普通变量一样给它赋值
  3. 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*,所以不管真实数据是 intstd::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");

执行过程可以理解为:

  1. 先构造一个临时 Any,里面保存 std::string("nihao")
  2. 临时对象和当前对象交换 _content
  3. 当前对象获得新数据
  4. 临时对象析构时释放旧数据

这样既完成了新数据替换,也避免了手动处理旧资源释放的复杂逻辑。

九、测试用例说明

为了验证 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,但它能让网络库的接口更加灵活,也让项目逐渐从"能跑"走向"更像一个可复用的网络库"。

相关推荐
海盗12341 小时前
.NET编码规范03-C#命名规范大全
linux·服务器·.net
WZF-Sang2 小时前
TCP和UDP协议
linux·服务器·网络·c++·学习·tcp/ip·udp
做个文艺程序员2 小时前
Linux第11篇:存储管理——磁盘分区、LVM 与文件系统
linux·运维·服务器
懂AI的老郑2 小时前
工业检测场景下智能体的应用:从自动化到智能化
运维·自动化
AI视觉网奇2 小时前
BambuStudio 自定义加载页面
服务器·前端·javascript
运维大师2 小时前
【云原生与DevOps】08-多云容灾架构设计:跨Region自动切换实践
运维·云原生·devops
一氧化二氢.h2 小时前
AI大模型批量自动化评测脚本
运维·人工智能·自动化
是枚小菜鸡儿吖2 小时前
服务器出问题别等用户反馈:用Python和钉钉搭一套轻量告警系统
服务器·python·钉钉
jarreyer10 小时前
【AI Agent】大模型自动化载体
运维·自动化