从此开始进入模板与泛型编程
,这也是一般C++
程序员较少接触的部分。如果读者有使用C++
里的标准模板库(Standard Template Librarry,STL)
的话,能够多多少少地体会到模板
的威力。
正如书上所说,这里的内容不能让读者成为专家级的template
程序员,但可以让读者成为一个比较好的template
程序员。笔者认为这里的意思是介绍一些模板编程的基本知识,例如更好地使用STL,或者进行一些简单的模板编程。因为一些更加进阶的玩法,例如在编译器利用模板编程完成某些运算等这些内容,要对模板元编程(Template Metaprogramming)
十分熟悉,这要求编程人员对C++
的语法特性十分了解,甚至要对准备编译源文件的编译器十分了解。
笔者自上个主题后期开始的条款就呈现出经验不足,对条款的解释能力开始下降,总结一下无非自己的编程经历也只能理解到书的前半部分,后面部分笔者仍然任重而道远。笔者在剩下的部分里尽自己最大的能力,不足的地方请多指教。
条款41:了解隐式接口和编译期多态
有关C++模板
编程的原理,读者们可以读《C++ Template》这本书了解。在开始介绍条款之前,读者们首先要了解模板是怎么编译的,例如:
1 | // 假定有个列表类 |
在编程中,这个类模板
被使用了:
1 | List<int> intList; |
在编译期,template是不会被编译的,而是先产生一个模板类
:
1 | // 一般会有mangling发生,例如这个模板类会名为IList之类的,但是这里不考虑mangling的问题 |
然后编译器才会去编译这个类模板
的实例。介绍这个原理的意义在于要说明,在类模板
有实例之前,类模板里面有什么内容,编译器是不会理会的,例如:
1 | template<typename T> |
如果来了一个Container<float>
,可想而知,当然是报错的。编译器也不会对类模板
以及模板类
有太多的检查,只要产生出来的模板类有符合模板类里面有的行为即可。笔者仿照书上的例子来说明结合隐式类型转换
,模板编程会变得多困难:
1 | class A |
看到输出:
1 | Greater than 10. |
也就是说,fun
里面,B的实例
是符合有size
函数这个约束的,而B::size()
返回的A类型
不能与int
直接比较,而是调用了A::operator int()
作了隐式转换之后比较。
这个例子要说明的是,模板推断
的双面性,一方面类型推断
的能力为编程人员带来了遍历,可以少写一些类型转换等语句,也就是语法糖吧;坏处就是推断太强大了,以至于做了隐式转换
可能产生非预期的结果。
而这个推断
是基于有效表达式
的。这也是书上要介绍的编译期多态
,也就是在编译器决议出用什么版本的函数。
条款42:了解typename的双重意义
在声明模板的时候:
1 | template<typename T> |
与:
1 | template<class T> |
里的typename
和class
是等价的关键字,但是在用到某个类模板
里的内置类型时,例如:
1 | std::vector<Object>::iterator iter; // 用到了vector里面的迭代器 |
然而有可能在编译过程中,编译器可能认为std::vector<Object>::iterator
是vector
里面的一个成员变量,而真实情况是vector
里面的一个嵌套从属类型
,编译器有可能会报错。这时改成:
1 | typename std::vector<Object>::iterator iter; |
问题解决,这时候的typename
是不能用class
关键字代替的,这里的typename
声明std::vector<Object>::iterator
是一个类型,而不是某种实例变量。
条款43:学习处理模板化基类内的名称
该条款涉及到:
笔者反复看这个条款,最后也完全理解这个条款想讲的是什么。但是推断应该是与类设计
相关的问题。引用书上的例子:
1 | class CompanyA |
接着为了让发送消息有日志,编写一个可以记日志的LoggingMsgSender
:
1 | template<typename Company> |
问题的根源在于,当编译器处理class template LoggingMsgSender
定义式时,首先要知道MsgSender
的定义式,但是MsgSender
是一个模板,也就是说需要一个MsgSender<Company>
的实例的定义式。而MsgSender
有可能有特化
的版本,特化
的版本与一般的模板又不一样,如:
1 | class CompanyZ // Z公司类 |
这个时候,LoggingMsgSender
:
1 | template<typename Company> |
因为MsgSender
有可能存在全特化的版本,因而编译器不会去处理基类相关的内容。
为了能够正常编译代码,有三个办法(假设所有的Company行为一致,不会像CompanyZ有与众不同的行为):
- 用
this
关键字1
2
3
4
5
6
7
8
9
10
11
12
13template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
...
void sendClearMsg(const MsgInfo& info)
{
... // 发送前记日志
this->sendClear(info); // 告诉编译器这个函数是类成员,强迫编译器从整个继承体系搜索
... // 发送后记日志
}
...
}; - 用
using
声明式:1
2
3
4
5
6
7
8
9
10
11
12
13
14template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
using MsgSender<Company>::sendClear; // 告诉编译器假定sendClear由基类提供
...
void sendClearMsg(const MsgInfo& info)
{
... // 发送前记日志
sendClear(info); // 假设sendClear存在于基类,则编译通过
... // 发送后记日志
}
...
}; - 显式指出函数的版本:第二三个方法显式地声明了函数,会使得
1
2
3
4
5
6
7
8
9
10
11
12
13template<typename Company>
class LoggingMsgSender : public MsgSender<Company>
{
public:
...
void sendClearMsg(const MsgInfo& info)
{
... // 发送前记日志
MsgSender<Company>::sendClear(info); // 告诉编译器用MsgSender<Company>::sendClear
... // 发送后记日志
}
...
};virtual
函数的多态行为失效。
还有,如果采用了解决方案之后,还出现LoggingMsgSender<CompanyZ>
的实例化,编译当然就失败了。
条款44:将与参数无关的代码抽离templates
该条款第一个提到的问题是非参数类型
带来的代码膨胀
,template
支持如下的语法:
1 | template<typename T, std::size_t size> // 假设该类为size*size大小的方阵 |
在写下:
1 | SquareMatrix<int, 3> m1; |
编译器会为SquareMatrix<int, 3>
以及SquareMatrix<int, 4>
模板类各自生成实例,可能产生如下的代码:
1 | class I3SquareMatrix |
当需要产生很多大小不一的方阵实例的时候,类模板
的实例代码随之膨胀。然而仅仅只是因为尺寸的问题就带来代码膨胀,这是不必要的。通过成员变量
一般可以解决问题:
1 | template<typename T> |
这样就解决了来自于非参数类型
导致的代码膨胀。
而对于来自与类型参数
的代码膨胀,一般不需要处理,要么就要有高超的编程技巧。例如SquareMatrix<int>
、SquareMatrix<const int>
和SquareMatrix<long>
明明在二进制表示
上一致(一般平台int和long一样),但是编译器会分别为它们生成不同的类模板实例。往往这样做的话需要用到void*
类型来操作一些手法,这个就是很底层的代码的问题了,而且STL
大部分容器是做了这个优化的,因而笔者认为该部分大多数人了解即可。
条款45:运用成员函数模板接受所有兼容类型
书上用了智能指针
来作说明,如下一个继承体系:
1 | class Top {...}; |
第一版的智能指针
类定义:
1 | template<typename T> |
原生的继承体系,用父类的指针指向子类实例:
1 | Top* top1 = new Middle; |
都没有任何问题,但是:
1 | void fun(SmartPtr<Top> top) |
但是问题就出现在这里了,原生的继承体系Top<-Middle<-Bottom
是没有问题的,一般的编译器会为上面的使用生成如下的类模板实例:
1 | // 模板的实例化 |
注意并不是:
1 | class MiddleSmartPtr : public TopSmartPtr |
也就是说SmartPtr<Middle>
与SmartPtr<Top>
没有任何关系,目前也没有代码提供它们之间的显式
或者隐式
类型转换,自然编译不能通过了。
为了解决这个问题,让SmartPtr<Top> top(SmartPtr<Middle>(new Middle))
这类的语法能够正常工作,SmartPtr
的类定义则应该这么写:
1 | template<typname T> |
更具体的设计可以查看std::shared_ptr
的源码,在这里要提示一点,在写下上面的成员函数的定义时,应该是:
1 | template<typename T> |
而不能写成不能通过的:
1 | template<typename T, typename U> |
这样就解决了泛化的问题。
条款46:需要类型转换时请为模板定义非成员函数
问题类似与条款24,不同的是这次加入了模板,假设有个有理数的类模板:
1 | template<typename T> |
根据常理,有理数直接的操作:
1 | Rational<int> oneHalf(1, 2); |
应该是没问题的,但是在强类型
语言中,编译器试图寻找匹配的函数,但是目前也只能匹配到:
1 | template<typename T> |
显然根据地一个参数,Rational<int>
被推导出来了,然而template实参推导过程
不会考虑通过构造函数而发生
的隐式类型转换。在编程人员尚未提供规则使得int
可以向const Rational<int>
转换,在产生模板函数过程自然推导失败了。
书上提供的解决方法:
1 | template<typename T> |
这样做的原理是让编译器在产生Rational<int>
类模板实例时,带上友元函数一起产生,使其省略上一个版本让编译器先为operator*
推导的步骤,然后迫使oneHalf * 2
套用函数时,对int
强制做隐式类型转换。
编译通过,但是会链接失败,神奇的是,把operator*
的定义放回类内部又可以了:
1 | template<typename T> |
friend
关键字原本是用于让类外部的函数,或者类,可以访问私有成员,但是这里并没有这样的使用。
当类模板相关的函数需要隐式类型转换的时候,将函数定义为类模板内部的friend函数
。
条款47:请使用traits classes表现类型信息
这个条款对于写轮子的人来说比较有用,STL
用的人应该不少,但是阅读过STL源码的人可能不多,连笔者至今也没有系统读过C++ STL的源码,而STL中才会有大量涉及traits classes
代码。
Traits并不是C++关键字或者一个预先定义好的构建:它们是一种技术,也是一个C++程序员共同遵守的协议。
因为模板编程的初衷是将与类型无关的操作提取出来,减少代码的重复性。不从实现角度来讨论,在该条款里面,模板编程表现得像接口
一样,引用书上advance
函数的例子:
1 | // advance作为一个STL算法实现,作用是让迭代器向前移动d步 |
在advance
例子里面,if
分支里面的描述用到的就是输入的迭代器的Traits
信息,假设迭代器的遵循Traits
协议:
1 | template<typename IterT> |
有了类似这样的实现,那么上面的advance
例子的判断语句可以具体为:
1 | if(IterT::is_random_access) |
这时的advance
就像是一个接口
,你只需要输入兼容的迭代器即可,无需关心该迭代器的特性会影响如何完成这个操作。
笔者前面的模拟实现是需要运行时的判断,而模板元编程强大之处,也是设计思想,尽可能地在编译期完成计算,提高运行时的效率。那么Traits
应该改成较为合理的形式(参考STL):
1 | template<...> // 省略参数 |
可以根据STL的设计,提供一个运行时的判断:
1 | template<typename IterT, typename DistT> |
为了优化运行时,同时不改变接口的情况下,这时候advance
更像一个接口了:
1 | template<typename IterT, typename DistT> |
这时的advance
仅仅是单纯转发(forwarding)
:
1 | template<typename IterT, typename DistT> |
以上介绍了Traits Classes
的基本原理和实现。
条款48:认识template元编程
模板元编程(Template Metaprogramming,TMP)
是在编译期
将运算完成,以此减少在运行时的运算,但是滥用模板
也会导致代码膨胀
。更多的详细介绍
在给出例子之前首先说明模板元编程
里面的循环是用递归
进行模拟的,读者也可以回想到模板里面没有直接支持模板循环的语法。
来一个阶乘计算的例子:
1 | template<unsigned n> |
然后写下:
1 | cout << Factorial<5>::value << endl; // 5! |
即可得到结果,当然TMP可以做更多的事情,但是也很考验程序员的设计能力。要是这么简单的话,TMP早就因其强大的编译期优化而流行了。
因此该条款也只是简单地介绍TMP,有兴趣的读者可以在网上搜索到更多的TMP奇淫技巧。