目录
项目介绍
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, ×, 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的赋值运算符重载调用了拷贝构造。