目录

项目介绍

HTTP服务器基本认识

Reactor模式基本认识

单Reactor单线程模式认识

单Reactor多线程模式认识

多Reactor多线程模式认识

模块划分

Server模块

Buffer模块

Socket模块

Channel模块

Connection模块

Acceptor模块

TimerQueue模块

Poller模块

EventLoop模块

TcpServer模块

通信连接管理模块关系图

监听连接管理模块关系图

事件监控管理模块关系图

前置知识

timerfd的认识与基本使用

时间轮定时器

时间轮定时器基本思想理解

时间轮定时器的设计完善

时间轮定时器的代码设计

正则表达式

正则表达式基本认识

正则表达式提取HTTP请求字段

通用型容器any

通用型容器any类设计思想

通用型容器any类代码设计


项目介绍

在这个项目中,我们要实现一个高并发服务器的组件,基于这个组件,我们可以快速地搭建一个高性能服务器。并且我们还提供了对应用层协议HTTP的支持。

所以,我们要实现这个项目,就需要了解:

  • 如何实现高并发服务器
  • 如何对HTTP协议进行支持

HTTP服务器基本认识

HTTP是一个应用层协议,在传输层是基于TCP实现的,所以搭建一个HTTP服务器本质上就是搭建一个TCP服务器,只不过传输数据的格式采用的是HTTP协议的格式。因此,实现HTTP服务器简单理解只需要以下几步:

  • 搭建一个TCP服务器,接收客户端请求。
  • 以HTTP协议格式进行解析请求数据,明确客户端目的。
  • 明确客户端请求目的后提供对应服务。
  • 将服务结果以HTTP协议格式进行组织,发送给客户端。

实现一个HTTP服务器很简单,但是实现一个高性能的服务器并不简单,因为未来可能会有非常多的客户端进行请求。搭建高性能的服务器,就需要使用Reactor模型了。

Reactor模式基本认识

Reactor就是事件驱动处理模式,就是会有多个客户端同时连接上服务器,服务端的处理是根据哪一个客户端触发了事件就去处理谁。服务端怎么知道哪一个客户端触发了事件呢?此时就使用到了IO多路转接。

  • 服务端思想:哪一个客户端触发了事件,也就是那个客户端发送了数据就处理谁。
  • 技术支撑点:IO多路转接。

单Reactor单线程模式认识

单Reactor单线程模式就是服务器在一个线程中完成IO事件监控和业务处理。

  • 优点:因为是单线程操作,操作都是串行化的,思想较为简单,编码流程也较为简单。
  • 缺点:因为所有的事件监控以及业务处理都是在一个线程中完成,因此很容易造成性能瓶颈。
  • 适用场景:客户端数量较少,且业务处理简单快速的场景。

单Reactor多线程模式认识

单Reactor多线程模式是一个Reactor线程 + 业务线程池。服务端有一个线程专门进行事件监控,以及IO。当有事件触发了,它不进行业务处理,它会读取客户端发送过来的数据,然后将数据交给业务线程池中的业务处理线程去处理,业务处理线程处理完后,将结果交给Reactor线程,再由它将响应发送给客户端。所以,Reactor线程只需要完成事件监控和IO操作即可,将业务处理分开了。

优点:充分利用了CPU多核资源,处理效率更高,降低了代码的耦合度。

缺点:在单个Reactor线程中,包含了对所有客户端的事件监控和IO处理,不利于高并发场景。比方说某一时刻有很多的客户端连接,可能来不及处理。

多Reactor多线程模式认识

多Reactor多线程模式也叫做主从Reactor模式。它主要是基于单Reactor多线程模式的缺点进行修改,单Reactor多线程模式中,Reactor线程在进行IO时,是没办法获取新连接的,所以,主从Reactor模式中将连接处理独立了出来。

主从Reactor模式 = 主Reactor线程 + 若干个从属Reactor线程 + 业务处理线程池。主从Reactor模式中会有一个主Reactor线程,这个线程是专门用来获取新连接的。会有多个从属Reactor线程,这些从属Reactor线程是用来进行IO事件监控和IO操作的,当主Reactor线程获取到一个新连接之后,会将这个新连接交给某一个从属Reactor线程,让其进行事件监控。当客户端发送了请求后,接收请求,然后将请求交给业务处理线程池进行处理,再由从属Reactor线程发送响应。

上面这种模式虽然能够解决单Reactor多线程模式存在的问题,但是我们要知道执行流不是越多越好,执行流多了,反而会增加CPU切换调度的成本。所以很多主从Reactor模式在设计时,不会涉及一个业务线程池,而是将业务处理在从属Reactor线程中完成。

我们的项目采用的是One Thread One Loop式的主从Reactor模式。One Thread One Loop的思想就是将一个连接的所有操作都放在一个线程中完成,一个线程对应一个事件处理的循环。也就是说,我们不要业务线程,所有的操作(IO事件监控 + IO处理 + 业务处理)都让从属Reactor线程完成。

模块划分

基于以上的理解,我们要实现的是一个带有协议支持的Reactor模型高性能服务器,因此将整个项目的实现划分为两个大的模块:

  • Server模块:实现Reactor模型的TCP服务器;
  • 协议模块:对当前的Reactor模型服务器提供应用层协议支持。

在这一篇文章中,我们只介绍Server模块,协议模块后序再介绍。

Server模块

Server模块就是对所有的连接以及线程进行管理,让它们各司其职,在合适的时候做合适的事,最终完成高性能服务器组件的实现。

而具体的管理也分为三个方面:

  • 监听连接管理:对监听连接进行管理。
  • 通信连接管理:对通信连接进行管理。
  • 超时连接管理:对超时连接进行管理。

基于以上的管理思想,将这个模块进行细致的划分又可以划分为以下多个子模块。

Buffer模块

Buffer模块是一个缓冲区模块,用于实现通信中用户态的接收缓冲区和发送缓冲区功能。

Socket模块

Socket模块是对套接字操作封装的一个模块,主要实现的socket的各项操作。

Channel模块

Channel模块主要是管理文件描述符的IO事件,并将事件分发到不同的回调函数进行处理。也就是管理服务器要监控这个文件描述符的什么事件,以及这个文件描述符触发了某个事件后,要如何进行处理,也就是调用什么回调函数。

我们在对一个文件描述符进行监控时,可能监控这个文件描述符的读事件、写事件、异常事件,Channel模块就是对文件描述符的IO事件进行管理,这样在用户层就很容易做到判断一个文件描述符监控了那些事件,并且也很容易去设置文件描述符监控的事件。我们还可以给Channel模块设置一些回调函数,当一个文件描述符触发了相应的事件就进行调用。

Connection模块

Connection模块是对Buffer模块、Socket模块、Channel模块的一个整体封装,实现了对一个通信套接字的整体的管理,每一个进行数据通信的套接字(也就是accept获取到的新连接)都会使用Connection进行管理。

Acceptor模块

Acceptor模块是对Socket模块、Channel模块的一个整体封装,实现了对一个监听套接字的整体的管理。当监听套接字获取到一个新连接时,实际上是一个文件描述符,Acceptor模块就可以将其封装成一个Connection,并给这个Connection设置各种回调。

TimerQueue模块

TimerQueue模块是实现固定事件定时任务的模块。

在上面3个模块中,监听连接、通信连接、超时连接的管理都已经有了,现在就差对这些连接的事件进行监控的模块了。

Poller模块

Poller模块是对epoll进行封装的一个模块,主要实现epoll的IO事件添加,修改,移除,获取活跃连接功能。

EventLoop模块

EventLoop模块可以理解就是我们上边所说的Reactor模块,它是对Poller模块,TimerQueue模块,Socket模块的一个整体封装,进行所有描述符的事件监控。

EventLoop模块为了保证整个服务器的线程安全问题,因此要求使用者对于Connection的所有操作一定要在其对应的EventLoop线程内完成,不能在其他线程中进行。一个Connection中是有发送缓冲区的,假设多个线程同时向发送缓冲区中写入,此时是有问题的,所以对于Connection的操作必须是线程安全的。而我们是采用One Thread One Loop的方式实现的,对于一个连接的所有操作都放在一个线程中完成,所以对于Connection的所有操作都应该放到EventLoop的线程内完成。

EventLoop模块保证自己内部所监控的所有描述符,都要是活跃连接,非活跃连接就要及时释放避免资源浪费。

  • 一个EventLoop是会监控多个连接的,所以内部需要维护一个任务队列。
  • 一个EventLoop中还会有一个定时任务队列,因为定时任务是对连接操作的,而对连接的所有操作都是在EventLoop的线程内操作。

EventLoop看到的就是一个一个的Connection。

TcpServer模块

这个模块是提供给组件使用者用来搭建服务器的模块。

通信连接管理模块关系图

Buffer、Socket、Channel都是独立功能模块,Connection是通信连接的管理模块。

监听连接管理模块关系图

对监听套接字管理,所以一定要有Socket;对监听套接字进行可读事件监控,所以一定要有Channel。

事件监控管理模块关系图

前置知识

timerfd的认识与基本使用

当一些客户端连接上服务器之后,一直不发送数据,这样会占据服务器的资源,显然是不好的。所以,应该定时地销毁一些非活跃的连接。此时就需要使用到定时器了。

#include <sys/timerfd.h>int timerfd_create(int clockid, int flags);

timerfd_create的作用是创建一个定时器。第一个参数:

  • CLOCK_REALTIME:表示以系统时间作为计时基准值(如果系统时间发生了改变就会出问题)
  • CLOCK_MONOTONIC:表示以系统启动时间进行递增的一个基准值(定时器不会随着系统时间改变而改变)

第二个参数传入0即可,表示阻塞。返回值是一个文件描述符。

Linux下一切皆文件,创建定时器本质也是创建一个文件。定时器的原理是每隔一段时间(超时时间),就会向文件中写入一个8字节的数据,这个数据表示的是从上一次读取这个文件到现在超时了几次。因为写入是一个8字节的数据,所以我们读取时一次也要读取8字节。

flags设置为0,就是定时器所关联的文件中没有数据时,就阻塞,直到超时了,里面有数据了再读取返回,这样的效果就是超时了就通知。如果是非阻塞的,当定时器所关联的文件中没有数据时,会出错返回,这与我们的要求不符合。

#include <sys/timerfd.h>int timerfd_settime(int fd, int flags, const struct itimerspec *new_value,struct itimerspec *old_value);struct timespec {time_t tv_sec;   // 秒数long   tv_nsec;  // 纳秒数(0 ≤ tv_nsec < 1e9)
};struct itimerspec {struct timespec it_interval;  // 第一次之后的超时间隔时间(定时器重复触发的时间间隔)struct timespec it_value;     // 第一次超时时间(定时器启动后多久首次触发)
};

timerfd_settime的作用是启动定时器。第一个参数是定时器标识符,也就是timerfd_create的返回值。第二个参数传入0即可,表示使用相对时间。第三个参数用于设置超时时间。第四个参数用于接收当前定时器原有的超时时间设置,主要用于还原,没有还原要求时传入nullptr即可。

int main()
{// 创建定时器int timerfd = timerfd_create(CLOCK_MONOTONIC, 0);if(timerfd < 0){perror("timerfd_create error");return -1;}// 设置定时器的超时时间struct itimerspec itime;itime.it_value.tv_sec = 2;itime.it_value.tv_nsec = 0;itime.it_interval.tv_sec = 1;itime.it_interval.tv_nsec = 0;timerfd_settime(timerfd, 0, &itime, nullptr);// 循环读取文件中的数据while(true){// 一次读取8字节uint64_t times;int ret = read(timerfd, &times, 8);if(ret < 0){perror("read error");return -2;}printf("超时了, 距离上一次超时了%ld次\n", times);}close(timerfd);return 0;
}

借助这个定时器就可以每隔1秒遍历一下所有的连接,看谁超时了就将连接关闭。

时间轮定时器

时间轮定时器基本思想理解

上面的定时器检测超时是需要遍历所有连接的,这样效率太低了。我们来看一种更加高效的定时器:时间轮定时器。

时间轮的思想来源于钟表,如果我们定了一个3点钟的闹铃,则当时针走到3的时候,就代表时间到了。同样的道理,如果我们定义了一个数组,并且有一个指针,指向数组起始位置,这个指针每秒钟向后走动一步,走到哪里,则代表哪里的任务该被执行了,那么如果我们想要定一个3s后的任务,则只需要将任务添加到tick+3位置,则每秒中走一步,三秒钟后tick走到对应位置,这时候执行对应位置的任务即可。

这个数组的大小就是最大定时时间。如果定时时间很大,岂不是要开一个很大的数组?是不需要的,此时可以定义秒级时间轮、分级时间轮、时级时间轮。每一个时间轮就是一个数组,秒级时间轮核分级时间轮的大小是60,时级时间轮的大小是24。

假设现在要定义一个1小时5分30秒的闹铃,先将1小时添加到时级时间轮,到了以后将5分钟添加到分级时间轮,到了以后将30秒添加到秒级时间轮,到了以后就说明该被执行了。

时间轮定时器的设计完善

我们来看看上面的设计中存在的一些问题:

1. 在同一时刻可能需要添加多个定时任务,所以需要将数组设计为二维数组。

2. 需要支持延时定时任务的功能。

我们重点看这个延时定时任务的功能要如何设计。作为一个时间轮定时器,本身并不会关心任务的类型,它只知道时间到了就执行即可。我们可以使用类的析构函数 + shared_ptr来实现延时定时任务的功能,具体做法是:

  • 使用一个类,对定时任务进行封装,类实例化的每一个对象,就是一个定时任务对象。将定时任务的执行放在析构函数中,这样任务对象被销毁时,就可以执行定时任务了。
  • shared_ptr用于对new的对象进行空间管理,当shared_ptr对一个对象进行管理的时候,内部会有一个引用计数,只有当计数器为0时,才会去释放这个对象。

当要执行某个定时任务时,就可以定义一个shared_ptr指向任务对象,当时间到了,指针对象被释放后,计数器为0,就可以通过析构函数调用要执行的任务了。

在延时的时候有一个点需要注意,我们如果通过shared_ptr去构造一个shared_ptr,是不会让前者的引用计数++的。所以我们需要为每一个定时任务指定一个ID,并使用weak_ptr管理创建的所有任务对象,维护好ID与weak_ptr的映射关系。weak_ptr不会占据shared_ptr的引用计数,通过它来管理原始对象,再通过它来构造shared_ptr就能使这些shared_ptr使用同一个引用计数了。

未来要延时某个任务时,只需要根据这个定时任务的ID,找到weak_ptr,由它来构造shared_ptr,并放到数组中即可,此时这个shared_ptr的引用计数就是2了。

时间轮定时器的代码设计

// 定时任务对象类
using TaskFunc = std::function<void()>;
using ReleaseFunc = std::function<void()>;
class TimerTask
{
public:TimerTask(uint64_t id, uint32_t delay, const TaskFunc &cb): _id(id), _timeout(delay), _task_cb(cb), _canceled(false) {}~TimerTask() {// 只有定时任务没有被取消才执行定时任务if(_canceled == false) _task_cb(); _release(); }void SetRelease(const ReleaseFunc &cb) { _release = cb; }uint32_t DelayTime() { return _timeout; }// 取消定时任务void Cancel() { _canceled = true; }
private:uint64_t _id;          // 定时任务对象的IDuint32_t _timeout;     // 定时任务的超时时间bool _canceled;        // 该定时任务是否被取消TaskFunc _task_cb;     // 定时任务对象要执行的定时任务ReleaseFunc _release;  // 删除TimerWheel中保存的定时任务对象信息
};// 时间轮定时器
class TimerWheel
{
public:TimerWheel():_capacity(60), _tick(0), _wheel(_capacity) {}// 添加定时任务void TimerAdd(uint64_t id, uint32_t delay, const TaskFunc &cb){PtrTask pt(new TimerTask(id, delay, cb));pt->SetRelease(std::bind(&TimerWheel::RemoveTimer, this, id));int pos = (_tick + delay) % _capacity;_wheel[pos].push_back(pt);_timers[id] = WeakTask(pt);}// 取消定时任务void TimerCancel(uint64_t id){auto it = _timers.find(id);if(it == _timers.end()){return ;}PtrTask pt = it->second.lock();if(pt) pt->Cancel();}// 延迟定时任务void TimerRefresh(uint64_t id){// 通过保存的定时任务对象的weak_ptr构造一个shared_ptr,并添加到时间轮中auto it = _timers.find(id);if(it == _timers.end()){return; }PtrTask pt = it->second.lock();int delay = pt->DelayTime();int pos = (_tick + delay) % _capacity;_wheel[pos].push_back(pt);}// 运转时间轮,让秒针每秒向后走一步void RunTimerTask(){_tick = (_tick + 1) % _capacity;_wheel[_tick].clear();}
private:// 将ID与weak_ptr的映射关系从_timers中移除void RemoveTimer(uint64_t id){auto it = _timers.find(id);if(it != _timers.end()){_timers.erase(it);}}
private:using WeakTask = std::weak_ptr<TimerTask>;using PtrTask = std::shared_ptr<TimerTask>;int _tick;      // 秒针int _capacity;  // 表盘容量,其实就是最大延迟时间std::vector<std::vector<PtrTask>> _wheel;// 保存定时任务ID与weak_ptr的映射关系,这里一定不能是shared_ptr// 否则shared_ptr的引用计数永远不为0std::unordered_map<uint64_t, WeakTask> _timers;
};

在这里要特别注意:取消定时任务不能在时间轮中实现,因为在时间轮中是对智能指针进行销毁,这样会导致对象被销毁,会调用析构函数,导致任务提前被执行。

我们来测试一下能否进行延时。

int main()
{TimerWheel tw;Test *t = new Test();// 向时间轮中添加一个任务,这个任务就是销毁指针ttw.TimerAdd(77, 5, std::bind(DelTest, t));for(int i = 0;i < 5;i ++){// 延迟定时任务sleep(1);tw.TimerRefresh(77);// 指针向后移动tw.RunTimerTask();std::cout << "刷新了定时任务, 重新需要5s才会销毁" << std::endl;}while(true){std::cout << "---------------------" << std::endl;sleep(1);tw.RunTimerTask();}return 0;
}

可以看到,成功进行了延时。

再来测试一下能否取消定时任务。

int main()
{TimerWheel tw;Test *t = new Test();// 向时间轮中添加一个任务,这个任务就是销毁指针ttw.TimerAdd(77, 5, std::bind(DelTest, t));for(int i = 0;i < 5;i ++){// 延迟定时任务sleep(1);tw.TimerRefresh(77);// 指针向后移动tw.RunTimerTask();std::cout << "刷新了定时任务, 重新需要5s才会销毁" << std::endl;}// 取消定时任务tw.TimerCancel(77);while(true){std::cout << "---------------------" << std::endl;sleep(1);tw.RunTimerTask();}return 0;
}

可以看到,是能够进行取消定时任务的。

正则表达式

正则表达式基本认识

正则表达式描述了一种字符串匹配的模式,可以用来检查一个串是否含有某种子串、将匹配的子串替换或者从某个串中取出符合某个条件的子串等。正则表达式的使用,可以使得HTTP请求的解析更加简单(这里指的时程序员的工作变得的简单,这并不代表处理效率会变高,实际上效率上是低于直接的字符串处理的),使我们实现的HTTP组件库使用起来更加灵活。

C++11提供了一个正则库regex。

bool regex_match(const std:string &src, std::smatch &matches, std::regex &e)

regex_match是用于完全匹配正则表达式的函数。

  • 第一个参数是原始字符串
  • 第二个参数是存放提取到的数据的容器
  • 第三个参数是一个正则表达式,表示进行匹配的规则
  • 返回值:用于确定匹配是否成功
int main()
{std::string str = "/numbers/1025";// 匹配以 /numbers/ 起始,后面根了一个或多个数字字符的字符串// 并且在匹配过程中提取这个匹配到的数字字符串std::regex e("/numbers/(\\d+)");std::smatch matches;bool ret = std::regex_match(str, matches, e);if(ret == false) return -1;for(auto &s : matches){std::cout << s << std::endl;}return 0;
}

可以看到,此时就成功提取到了匹配的字符串。解释一下上面的正则表达式:

  • /numbers/:表示匹配以这个开头的字符串
  • \\d:匹配任意数字(0 - 9)
  • +:表示数字可以出现1次或多次
  • ():捕获分组,提取匹配到的数字

正则表达式提取HTTP请求字段

假设我们现在有一条HTTP请求:

GET /api/products?category=electronics&page=2&limit=20&sort=price_desc HTTP/1.1\r\n

我们要从中提取请求方法、请求路径、参数、协议版本。

正则表达式提取HTTP请求方法

int main()
{std::string str = "GET /api/products?category=electronics&page=2&limit=20&sort=price_desc HTTP/1.1";std::smatch matches;// 请求方法:GET HEAD POST PUT DELETEstd::regex e("(GET|HEAD|POST|PUT|DELETE) .*");bool ret = std::regex_match(str, matches, e);if(ret == false) return -1;for(auto &s : matches){std::cout << s << std::endl;}return 0;
}

正则表达式提取HTTP请求路径

int main()
{std::string str = "GET /api/products?category=electronics&page=2&limit=20&sort=price_desc HTTP/1.1";std::smatch matches;// 请求方法:GET HEAD POST PUT DELETEstd::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*).*");bool ret = std::regex_match(str, matches, e);if(ret == false) return -1;for(auto &s : matches){std::cout << s << std::endl;}return 0;
}

正则表达式提取HTTP请求参数

int main()
{std::string str = "GET /api/products?category=electronics&page=2&limit=20&sort=price_desc HTTP/1.1";std::smatch matches;// 请求方法:GET HEAD POST PUT DELETEstd::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)\\?(.*) .*");bool ret = std::regex_match(str, matches, e);if(ret == false) return -1;for(auto &s : matches){std::cout << s << std::endl;}return 0;
}

正则表达式提取HTTP请求版本

int main()
{std::string str = "GET /api/products?category=electronics&page=2&limit=20&sort=price_desc HTTP/1.1";std::smatch matches;// 请求方法:GET HEAD POST PUT DELETE// HTTP请求的版本一般是1.0或1.1std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)\\?(.*) (HTTP/1\\.[01]).*");bool ret = std::regex_match(str, matches, e);if(ret == false) return -1;for(auto &s : matches){std::cout << s << std::endl;}return 0;
}

正则表达式提取HTTP请求字段细节

1. HTTP请求的最后可能会有\n,或\r\n,或者什么都没有。

int main()
{std::string str = "GET /api/products?category=electronics&page=2&limit=20&sort=price_desc HTTP/1.1\r\n";std::smatch matches;// 请求方法:GET HEAD POST PUT DELETE// HTTP请求的版本一般是1.0或1.1std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)\\?(.*) (HTTP/1\\.[01])(?:\n|\r\n)");bool ret = std::regex_match(str, matches, e);if(ret == false) return -1;for(auto &s : matches){std::cout << s << std::endl;}return 0;
}

2. 一个HTTP请求中可能不包含参数。

int main()
{std::string str = "GET /api/products?category=electronics&page=2&limit=20&sort=price_desc HTTP/1.1\r\n";std::smatch matches;// 请求方法:GET HEAD POST PUT DELETE// HTTP请求的版本一般是1.0或1.1std::regex e("(GET|HEAD|POST|PUT|DELETE) ([^?]*)(?:\\?(.*))? (HTTP/1\\.[01])(?:\n|\r\n)?");bool ret = std::regex_match(str, matches, e);if(ret == false) return -1;for(auto &s : matches){std::cout << s << std::endl;}return 0;
}

通用型容器any

通用型容器any类设计思想

每一个Connection对连接进行管理,最终都不可避免需要涉及到应用层协议的处理,因此在Connection中需要设置协议处理的上下文来控制处理节奏。但是应用层协议千千万,为了降低耦合度,这个协议接收解析上下文就不能有明显的协议倾向,它可以是任意协议的上下文信息,因此就需要一个通用的类型来保存各种不同的数据结构。这里的上下文不是指Socket缓冲区的内容,而是缓冲区的内容解析后的字段。所以:

  • 一个连接必须拥有一个请求接收与解析的上下文。保存数据的处理状态,以便下次有新数据到来时能够继续处理。
  • 上下文的类型或者说结构不能固定,因为服务器支持的协议有可能会不断增多。不同的协议,可能都会有不同的上下文结构。

结论:必须拥有一个容器,能够保存各种不同的类型结构数据。

在C语言中,通用类型可以使用void*来管理,但是在C++中,boost库和C++17给我们提供了一个通用类型any来灵活使用,如果考虑增加代码的移植性,尽量减少第三方库的依赖,则可以使用C++17特性中的any,或者自己来实现。在我们的项目中,我们采用自己实现的any。

通用型容器any类代码设计

我们想要的效果是定义出一个any对象后,这个对象内部可以存储任意类型的数据。

Any a;
a = 77;
a = "abc";

所以我们不能将Any设计为模板类。

class Any
{
private:class holder{}template<class T>class placeholder : public holder{T _val;}holder *content;
};

Any类中保存的是holder类的指针,当要使用Any保存一个数据时,只需要使用这个数据去实例化出一个子类对象,然后通过父类指针指向这个对象即可。

class Any
{
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; }// 返回子类对象保存数据的指针template<class T>T *get(){// 想要的数据类型,必须和保存的数据类型一致assert(typeid(T) == _content->type());return &((placeholder<T>*)_content)->_val;}Any &swap(Any &other){std::swap(_content, other._content);return *this;}// 赋值运算符重载template<class T>Any& operator=(const T &val){// 为val构造一个临时的通用容器,然后与当前容器自身进行指针交换// 临时对象释放的时候,原先保存的数据也会被释放Any(val).swap(*this);return *this;}Any& operator=(const Any &other){Any(other).swap(*this);return *this;}
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;  // Any容器保存的值};holder *_content;
};

测试一下能否存放不同类型的数据。

int main()
{Any a;a = 77;int *pa = a.get<int>();std::cout << *pa << std::endl;a = std::string("hello");std::string *ps = a.get<std::string>();std::cout << *ps << std::endl;return 0;
}

测试一下是否存在内存泄漏。

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;{Test t;a = t;}return 0;
}

这里会出现两次析构是因为Any的赋值运算符重载调用了拷贝构造。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/96194.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/96194.shtml
英文地址,请注明出处:http://en.pswp.cn/web/96194.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

lua中table键类型及lua中table的初始化有几种方式

在 Lua 中&#xff0c;table 的键几乎可以是任何类型&#xff0c;但有几个重要的规则和最佳实践需要了解。1. 主要键类型(1) 字符串 (string)这是最常见、最推荐的键类型。local person {name "Alice", -- 等同于 ["name"] "Alice"["age…

matlab实现利用双MZI产生RZ33-QPSK信号

利用MATLAB实现双MZI产生RZ33-QPSK信号的代码&#xff1a; 参数设置 % 信号参数 fs 1e6; % 采样频率 fc 10e6; % 载波频率 T 1e-6; % 符号周期 N 1000; % 采样点数 t 0:1/fs:(N-1)/fs; % 时间向量生成QPSK信号 % 生成随机二进制序列 data randi([0,1],1,N);% 将二进制序列…

Vue响应式更新 vs React状态更新:两种范式的底层逻辑与实践差异

在现代前端框架中&#xff0c;Vue和React作为两大主流选择&#xff0c;分别采用了截然不同的状态管理与更新机制。Vue的“响应式更新”通过自动追踪依赖实现数据与视图的联动&#xff0c;而React的“状态更新”则依赖显式setState触发重新渲染。本文将从底层原理、更新流程、优…

Spring MVC 的常用注解

一、控制器相关注解ControllerController注解用于标记一个类为 Spring MVC 的控制器。在 Spring MVC 框架里&#xff0c;控制器扮演着关键角色&#xff0c;负责接收 HTTP 请求并返回响应。当一个类被Controller注解标记后&#xff0c;Spring 容器会自动识别并将其纳入管理。例如…

Oracle APEX 利用卡片实现翻转(方法一)

目录 0. 以 Oracle 的标准示例表 EMP 为例&#xff0c;实现卡片翻转 1. 创建PL/SQL动态内容区域 2. 添加 CSS 实现翻转效果 3. 添加动态操作 (Dynamic Action) 4. 看效果 0. 以 Oracle 的标准示例表 EMP 为例&#xff0c;实现卡片翻转 正面&#xff1a; 显示员工姓名 (EN…

Gradio全解11——Streaming:流式传输的视频应用(1)——FastRTC:Python实时通信库

Gradio全解11——Streaming&#xff1a;流式传输的视频应用&#xff08;1&#xff09;——FastRTC&#xff1a;Python实时通信库前言第11章 Streaming&#xff1a;流式传输的视频应用11.1 FastRTC&#xff1a;Python实时通信库11.1.1 WebRTC协议与FastRTC介绍1. WebRTC协议的概…

一文学会二叉搜索树,AVL树,红黑树

文章目录二叉搜索树查找插入删除AVL树概念插入旋转AVL验证红黑树概念插入检测二叉搜索树 也称二叉排序树或二叉查找树 二叉搜索树&#xff1a;可以为空&#xff0c;若不为空满足以下性质 ⭐1&#xff0c;非空左子树小于根节点的值 ⭐2&#xff0c;非空右子大于根节点的值 ⭐3…

Android实战进阶 - 启动页

场景&#xff1a;当启动页处于倒计时阶段&#xff0c;用户将其切换为后台的多任务卡片状态&#xff0c;倒计时会继续执行&#xff0c;直到最后执行相关逻辑&#xff08;一般会跳转引导页、进入主页等&#xff09; 期望&#xff1a;而综合市场来看&#xff0c;一般我们期望的是当…

无标记点动捕技术:重塑展厅展馆的沉浸式数字交互新时代

在元宇宙浪潮的持续推进下&#xff0c;虚拟数字人正逐渐成为连接虚实世界的重要媒介。在展厅展馆中&#xff0c;数字人不仅能够扮演导览员、讲解员角色&#xff0c;更可通过情感化交互提升参观体验&#xff0c;使文化传播更具感染力和沉浸感。虚拟人的引入&#xff0c;为传统展…

轻松Linux-7.Ext系列文件系统

天朗气清&#xff0c;惠风和煦&#xff0c;今日无事&#xff0c;遂来更新。 1.概述 总所周知&#xff0c;我们存的数据都是在一个叫硬盘的东西里面&#xff0c;这个硬盘又像个黑盒&#xff0c;这章就来简单解析一下Linux中文件系统。 现在我们用的大都是固态硬盘&#xff0c;…

Matlab机器人工具箱使用4 蒙特卡洛法绘制工作区间

原理&#xff1a;利用rand随机数&#xff0c;给各个关节设置随机关节变量&#xff0c;通过正运动学得到末端位姿变换矩阵&#xff0c;然后利用变换矩阵2三维坐标标记出末端坐标&#xff0c;迭代多次就可以构成点云。教程视频&#xff1a;【MATLAB机器人工具箱10.4 机械臂仿真教…

【项目】在AUTODL上使用langchain实现《红楼梦》知识图谱和RAG混合检索(三)知识图谱和路由部分

首先在数据集 - 开放知识图谱下载红楼梦的知识图谱&#xff0c;这个网站上有各种各样的知识图谱&#xff0c;可以挑你感兴趣的做( • ̀ω•́ ) 这个知识图谱的作者们已经将三元组抽取出来了&#xff0c;我们可以直接用&#xff0c;如果你对三元组是如何生成的感兴趣&#xf…

pycharm 最新版上一次编辑位置

2025nipycharm方法一&#xff1a;用快捷键&#xff08;最方便&#xff09;跳回上一次编辑位置&#xff1a;Windows/Linux: Ctrl Alt ←macOS: ⌘ Option ←跳到前一次位置&#xff1a;Windows/Linux: Ctrl Alt →macOS: ⌘ Option →方法二&#xff1a;显示工具栏按钮在…

前端性能监控与优化:从 Lighthouse 到 APM

在当今竞争激烈的数字环境中&#xff0c;用户对Web应用性能的要求日益提高。一个缓慢或响应迟钝的应用不仅会流失用户&#xff0c;更可能损害品牌形象和商业价值。因此&#xff0c;前端性能的监控与优化已成为前端开发不可或缺的关键环节。本文将深入探讨从基础的性能评估工具L…

TC_Motion多轴运动-电子齿轮

目录 电子齿轮 【基本概念】 【应用示例】 【开发总结】 END 电子齿轮 【基本概念】 定义:通过软件方法实现机械齿轮的速比调节功能(两个轴成线性比例旋转) 优点 免维护,告别机械损耗 易调节,任意修改齿轮比 精度高,无机械背隙 应用场景 多台电机拖动同一负载,要求多台…

CentOS 7 下载教程

访问阿里云镜像站 阿里巴巴开源镜像站 选择centos 点这个 选择7版本 进入isos目录 点这个 选择这个版本 因为这个镜像的日期更新 推荐下载 DVD 版&#xff1a;包含完整系统和常用软件&#xff0c;无需额外联网安装组件Minimal 版&#xff1a;精简版&#xff0c;仅包含基础系…

MAC在home下新建文件夹报错“mkdir: test: Operation not supported”

在Mac电脑中&#xff0c;home文件夹下不能直接mkdir&#xff0c;sudo 也还是不行&#xff0c;提示“mkdir: test: Operation not supported”。网上找的解决方案不好使&#xff0c;因为没有关闭系统完整性保护关闭系统完整性保护查看SIP当前的状态csrutil status如果是开启状态…

交叉导轨从测试仪到工作台的精密运动控制

在精密仪器领域微米级的运动精度与纳米级的稳定性往往是决定设备性能上限的核心指标。而支撑这一技术鸿沟跨越的&#xff0c;往往隐匿于机械结构的“毫厘之间”——交叉导轨。以下是其在不同精密仪器中的具体应用&#xff1a;光学测试仪&#xff1a;光学测试仪主要用于各种高精…

内网穿透的应用-Navidrome与cpolar本地搭建跨网络访问的云音乐服务器

文章目录前言1. 安装Docker2. 创建并启动Navidrome容器3. 公网远程访问本地Navidrome3.1 内网穿透工具安装3.2 创建远程连接公网地址3.3 使用固定公网地址远程访问前言 音乐收藏存在平台版权限制、音质压缩和访问不便等问题。Navidrome 开源音乐服务器与 cpolar 内网穿透服务的…

FastAPI 访问不了API文档或配置不生效的解决方法

FastAPI中文教程 本文背景 FastAPI框架自带交互式api文档,通过路由/docs或者/redoc 访问&#xff0c;但是FastAPI 的文档界面&#xff08;如 /docs 和 /redoc&#xff09;依赖于外部的 JavaScript 和 CSS 库&#xff0c;如果项目部署环境网络不佳或者无法访问外网的时候&…