计算机 · 2021年5月23日 0

深入应用C++11笔记 第11章

使用C++11开发一个轻量级的IoC容器

在工作中遇到一个与IoC容器类似的问题,那就是不同线程之间通信的时候想通过同一个队列传递不同类型的数据,而这些数据的类型并没有继承关系,不属于同一个基类的子类。最先想到的是使用union来保存不同类型。然而使用union来保存非POD类型的数据是有问题的,因为union在构造和析构的时候是不知道要怎么处理union里的数据,union并不知道其中保存的数据到底是哪个类型的,自己动手尝试写一个这样的union,编译的时候就会报错,提示union的构造函数和析构函数是deleted,因此需要自己手动添加union的构造函数的实现。同样的对于union的拷贝函数也会存在问题,需要自己添加union拷贝函数的实现避免浅拷贝带来的问题。另外union里没有地方保存当前active的类型,因此需要在union之外增加变量来保存union里有效的类型,于是就要自己实现一个tagged union类来把这个union给包起来。在实现这个tagged union类的过程中,也会用到一点平时不用的东西,那就是inplacement new和手动调用析构函数。具体如何实现这样一个简单的tagged union类,可以读知乎上的这篇文章。但是这样一个简单的tagged union类是不够的,因为如果我们想重用代码,改变存放在这个union里的类型,那么就必须用模板来抽象和简化我们的代码,我们不想往这个tagged union里新增一个类型时就要几乎把这个tagged union类的每个函数实现都改一遍。于是我们的需求实际上就变成了实现c++17里的variant。可以去翻c++17 variant的实现,或者参考这篇博客。variant方案的缺点(也可以说是优点)是需要在一开始的模板参数里列出所有可能作为成员的类型。除了使用union来保存不同类型,也可以使用void*来保存,这其实就是c++17里新增的any了。variant可以看作是改进的union,any可以看作是改进的void *,不直接使用union和void*除了前面讲的构造、析构函数、拷贝函数带来的问题,还有就是模板的问题,我们不想用硬编码,我们想要一个方便好用可重用性高的union和void*,于是就有了variant和any。关于variant和any的区别,可以查看stackoverflow上的一个讨论

  • variant支持的类型是一开始编译前就定死的,any是动态的,什么类型都可以
  • variant类型检查是O(1)复杂度,any类型检查是O(n)
  • variant的类型声明可以让人更好地理解打算用这个variant保存哪些类型的东西

除非必要,优先使用variant。

回到书中所讲的内容,作者要解决的问题是实现一个工厂类或者函数,这个工厂可以返回不同类型的对象,而且要返回的类型之间可能没有公共的基类。另外希望修改工厂能够返回的类型时不需要修改这个工厂类或者函数的代码,实现可配置或者所谓的控制反转(Inversion of Control, IoC)。
要返回不同的类型涉及到类型擦除的技术,作者总结了下类型擦除的常用方式:

  1. 通过多态
    缺点是要返回的不同类型必须继承于同一个基类
  2. 利用模板
    缺点是模板实例化的时候始终得指定一个基本类型,相当于还是得设计个基类
  3. 通过某种类型容器来擦除类型
    就是使用variant,缺点是要一开始指定所有将要包含的类型
  4. 通过某种通用类型来擦除
    就是使用any
  5. 通过闭包来擦除类型
    作者举了下面这样一段示例代码,可以看出并没有关于类型的硬编码:
   template<typename T>
   void Func(T t)
   {
     cout << t << endl;
   }
   void TestErase()
   {
     int x = 1;
     char y = 's';

     vector<std::function<void()>> v;
     //类型擦除,闭包中隐藏了具体的类型,将闭包保存起来
     v.push_back([x]{Func(x);});
     v.push_back([y]{Func(y);});
     //遍历闭包,从闭包中取出实际的参数,并打印出来
     for (auto item : v)
     {
       item();
     }
   }

作者选用了any和闭包来实现一个这样的工厂,思路就是在工厂内部创建一个string到any的map,string用来作为key指明要采用什么样的构造函数来创建对象,any则保存相对应的构造函数的闭包。为了能够支持带参数的构造函数,作者又利用了可变模板参数的特性,将无参构造函数和带参数的构造函数形式上统一了起来。最后作者还利用enable_if实现了对于没有继承关系的两个类型创建依赖对象,而对于有继承关系的两个类型创建派生类的对象。

本章作者的代码实现,不过没有enable_if的版本,这个只有看书上的实现了。