第7章:模板与泛型编程
条款41:了解隐式接口和编译期多态
哪一个重载函数被调用——发生在编译期
哪一个
virtual函数被绑定——发生在运行期class和template都支持接口和多态对
class而言,接口是显式的,以成员函数的签名为中心,多态则通过virtual函数发生在运行期。对template而言,接口是隐式的,以有效表达式为中心,多态则通过template具现化和函数重载解析发生于编译期。
条款42:了解typename的双重意义
当我们声明模板类型参数,
class和typename的意义完全相同1
2template<typename T>
template<class T>typename用来标识嵌套从属类型名称template内出现的名称如果相依于某个template参数,称这个名称为从属名称,如果丛属名称在class内呈嵌套状,称为嵌套从属名称。如果嵌套从属名称还指涉某种类型名称为嵌套从属类型名称1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// std::iterator_traits<IterT>::value_type 就是嵌套从属类型名称
template<typename IterT>
void workWithIterator(IterT iter)
{
// 必须使用 typename 关键字标识
typename std::iterator_traits<IterT>::value_type temp(*iter);
...
}
// 使用 typedef 少打几个字
template<typename IterT>
void workWithIterator(IterT iter)
{
// 必须使用 typename 关键字标识
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
...
}不允许在
base class list或member initialization list内使用typename作为base class的标识符
条款43:学习处理模板化基类内的名称
假设我们需要撰写一个MsgSender类,它能够传送信息到若干不同的Company去。信息要不进行加密后的密文,要不就是未经加工的文字。如果编译期间我们有足够信息来决定哪一个信息传至哪一家公司,就可以采用基于template的解法:
1 | class CompanyA |
现在假设我们想要在每次送出信息时log某些信息。derived class可轻易提供解决方法:
1 | template<typename Company> |
编译器会抱怨sendClear函数不存在,明明在那里,为什么?
问题在于,当编译器看见class template LoggingMsgSender定义式时,并不知道它继承什么样的class。当然它继承的是MsgSender<Company>,但其中的Company是个template参数,不当LoggingMsgSender被具现化之前无法确切知道它是什么。而如果不知道Company是什么,就无法知道class MsgSender<Company>看起来像什么——更明确地说是没办法知道它是否有个sendClear函数。
具体点说,假设现在有个CompanyZ坚持只使用加密通讯:
1 | // 不提供 sendCleartext 函数 |
一般性的MsgSender template对CompanyZ并不合适,因为那个template提供了一个sendClearMsg函数,而这对CompanyZ对象并不合理。欲矫正这个问题,我们可以针对CompanyZ产生一个MsgSender特化版:
1 | // 全特化版本只提供 sendSecret 函数 |
这就解释了前面编译器拒绝那个调用的原因:它知道base class template有可能被特化,而那个特化版本可能不提供和一般性template相同的接口。因此它往往拒绝在templatized base class(本例的MsgSender<Company>)内寻找继承而来的名称(本例的SendClear)。
- 当我们从
Object Oriented C++跨进Template C++(见条款1) ,继承就不像以前那样畅行无阻了
有三个解决令C++编译器“不进入templatized base class观察”的行为失效的办法:
使用
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)
{
... // 将“传送前”的信息写至 log
this->sendClear(info); // 调用 base class 函数
... // 将“传送后”的信息写至 log
}
...
};使用
using声明虽然
using声明式在在条款33或在这里都可有效运作,但两处解决的问题其实不相同。前者是base class名称被derived class名称遮掩,而后者是编译器不进入base class作用域内查找,于是我们通过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;
... // 构造函数、析构函数等等
void sendClearMsg(const MsgInfo& info)
{
... // 将“传送前”的信息写至 log
sendClear(info); // 调用 base class 函数
... // 将“传送后”的信息写至 log
}
...
};使用作用域运算符
::明确指定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)
{
... // 将“传送前”的信息写至 log
MsgSender<Company>::sendClear(info); // 调用 base class 函数
... // 将“传送后”的信息写至 log
}
...
};
即使如此,如果稍后这样做:
1 | LoggingMsgSender<Companyz>zMsgSender; |
其中对sendClearMsg的调用动作将无法通过编译,因为在那个点上,编译器知道base class是个template特化版本MsgSender<CompanyZ>,而且它知道那个class不提供sendClear函数,而sendClear却是sendClearMsg尝试调用的函数。
条款44:将与基类无关的代码抽离template
当你编写某个函数,其中某些部分的实现码和另一个函数的实现码实质相同,你会抽出两个函数的共同部分,把它们放进第三个函数中,然后令原先两个函数调用这个新函数。如果你正在编写某个class,而你明白其中某些部分和另一个class的某些部分相同,你也不会重复这共同的部分。你会把共同部分搬移到新class去,然后使用继承或复合(见条款32,38,39) ,令原先的class取用这共同特性。而原class的互异部分仍然留在原位置不动。
编写template时,也是做相同的分析,以相同的方式避免重复。然而在template代码中,重复是隐晦的:毕竟只存在一份template源码,所以你必须训练自己去感受当template被具现化多次时可能发生的重复。
举个例子,假设你想为固定尺寸的正方矩阵编写一个template。该矩阵的性质之一是支持逆矩阵运算。
1 | // template 支持 n x n 矩阵, 元素类型为 T |
这会具现化两份invert。这些函数并非完完全全相同,因为其中一个操作的是5x5矩阵而另一个操作的是10x10矩阵,但除了常量5和10,两个函数的其他部分完全相同。这是template引出代码膨胀的一个典型例子。
第一次修改:
1 | // 与尺寸无关的 base class |
这里的base class只是为了帮助derived class实现,不是为了表现SquareMatrix和SquareMatrixBase之间的is-a关系(关于private继承,见条款39)。
目前为止一切都好,但还有一些棘手的题目没有解决。SquareMatrixBase::invert如何知道该操作什么数据?虽然它从参数中知道矩阵尺寸,但它如何知道哪个特定矩阵的数据在哪儿?想必只有derived class知道。derived class如何联络其base class做逆运算动作?一个可能的做法是为SquareMatrixBase::invert添加另一个参数,可以是个指针,指向一块用来放置矩阵数据的内存地址。
第二次修改:
1 | template<typename T> |
这个条款只讨论由non-type template parameter(非类型模板参数)带来的膨胀,其实type parameter(类型参数)也会导致膨胀。例如在许多平台上int和 long有相同的二进制表述,所以像vector<int>和vector<long>的成员函数有可能完全相同。某些链接器(linker)会合并完全相同的函数实现码,但有些不会,后者意味某些template被具现化为int和long两个版本,并因此造成代码膨胀。类似情况,所有指针类型都有相同的二进制表述,因此凡template持有指针者(例如list<int*>,list<const int*>,list<SquareMatrix<long, 3>*>等等)往往应该对每一个成员函数使用唯一一份底层实现。这很具代表性地意味,如果你实现某些成员函数而它们操作强型指针(strongly typed pointer,即T*),你应该令它们调用另一个操作无类型指针(untyped pointer,即void*)的函数,由后者完成实际工作。某些C++标准程序库实现版本的确为vector,deque和list等template做了这件事。
条款45:成员函数模板接受所有兼容类型
内置指针是支持隐式转换的,比如derived class的指针可以隐式转换为base class指针,指向non-const对象的指针可以转换为指向const对象等等。
1 | class Top {...}; |
但是我们自己实现的智能指针模板类,想做到这样就稍稍有点麻烦了。我们希望下面的代码能够通过编译:
1 | temmplate<typename T> |
注意,这些赋值表达式需要调用的都是copy构造函数。这里的代码不能通过编译,因为如果以带有base-derived关系的B,D两类型分别具现化某个template,产生出来的两个具现体并不带有base-derived关系,所以编译器视SmartPtr<Middle>和SmartPtr<Top>为完全不同的class,为了获得我们希望获得的SmartPtr class之间的转换能力,我们必须将它们明确地编写出来。
我们应该为它写一个构造模板。这样的模板是所谓member function template,其作用是为class生成函数:
1 | template<typename T> |
我们称之为泛化(generalized)copy构造函数。它并未被声明为explicit,那是因为内置指针类型之间的转换(例如从derived class指针转为base class指针)是隐式转换,所以让智能指针按照这种形式也属合理。
但是,这个为SmartPtr而写的“泛化copy构造函数”提供的东西比我们需要的更多。我们只希望根据一个SmartPtr<Bottom>创建一个SmartPtr<Top>,却不希望根据一个SmartPtr<Top>创建一个SmartPtr<Bottomr>,因为那对public继承而言(见条款32)是矛盾的。我们也不希望根据一个SmartPtr<double>创建一个SmartPtr<int>,我们必须从某方面对这一member template所创建的成员函数群进行挑拣。
假设SmartPtr遵循std::shared_ptr也提供一个get成员函数,返回智能指针对象所持有的那个原始指针的副本,那么我们可以在“构造模板”实现代码中约束转换行为,使它符合我们的期望:
1 | template<typename T> |
这个行为只有当“存在某个隐式转换可将一个U*指针转为一个T*指针”时才能通过编译,而这正是我们想要的。
member function template(成员函数模板)的效用不限于构造函数,它常扮演的另一个角色是支持赋值操作。例如std::shared_ptr同时支持所有“来自兼容之内置指针、std::shared_ptr、std::weak_ptr”的构造行为以及赋值操作(std::weak_ptr除外)。赋值操作符和copy构造函数实现类似,故省略。
- 如果你声明
member template用于“泛化copy构造”或“泛化assignment操作”你还是需要声明正常的copy构造函数和copy assignment操作符
条款5说过,编译器可能为我们产生四个成员函数,其中两个是copy构造函数和copy assignment操作符。现在,SmartPtr声明了一个泛化copy构造函数,而显然一旦类型T和U相同,泛化copy构造函数会被具现化为“正常的”copy构造函数。那么究竟编译器会暗自为SmartPtr生成一个copy构造函数呢?或当某个SmartPtr对象根据另一个同型的SmartPtr对象展开构造行为时,编译器会将“泛化copy构造函数模板”具现化呢?
member template并不改变语言规则:“如果程序需要一个copy构造函数,你却没有声明它,编译器会为你暗自生成一个”。在class内声明泛化copy构造函数(是个member template)并不会阻止编译器生成它们自己的copy构造函数(一个non-template),所以如果你想要控制copy构造的方方面面,你必须同时声明泛化copy构造函数和“正常的”copy构造函数。相同规则也适用于赋值操作。
条款46:需要类型转换时请为模板定义非成员函数
条款24讨论过为什么惟有non-member函数才有能力“在所有实参身上实施隐式类型转换”,该条款并以Rational class的operator*函数为例。本条款将Rational和operator*模板化:
1 | template<typename T> |
就像条款24一样,我们也希望支持混合式算术运算,然而以下代码却不能通过编译:
1 | Rational<int> oneHalf(1, 2); |
在这里,编译器不知道我们想要调用哪个函数!它们试图找到什么函数被名为operator*的template具现化出来。它们知道它们应该可以具现化某个“名为operator*并接受两个Rational<T>参数”的函数,但为完成这一具现化行动,必须先算出T是什么。
为了推导T,它们看了看operator*调用动作中的实参类型。分别是Rational<int>(oneHalf的类型)和int(2的类型)。
以oneHalf进行推导,过程并不困难。第二参数的推导则没有这么顺利。operator*的第二参数被声明为Rational<T>,但传递给operator*的类型是int(2)。编译器如何根据这个2推算出T?你或许会期盼编译器使用Rational<int>的non-explicit构造函数将转换为Rational<int>,进而将T推导为int,然而template实参推导过程中并不考虑采纳“通过构造函数而发生的”隐式类型转换。
template class内的friend声明式可以指涉某个特定函数。这意味class Rational<T>可以声明operator*是它的一个friend函数。class template并不倚赖template实参推导,所以编译器总是能够在class Rational<T>具现化时得知T。因此:
1 | template<typename T> |
现在当对象oneHalf被声明为一个Rational<int>,class Rational<int>于是被具现化出来,而作为过程的一部分,friend函数operator*(接受Rational<int>参数)也就被自动声明出来。后者身为一个函数而非function template,因此编译器可在调用它时使用隐式转换函数(例如调用Rational的non-explicit构造函数)。
但是,虽然这段代码通过编译,却无法连接。
因为这个友元函数只被声明于Rational内,并没有被定义出来。我们意图令此class外部的operator* template提供定义式,是行不通的。既然我们没有提供定义式,连接器当然找不到它!
最简单的方法就是,将函数本体放进class内:
1 | template<typename T> |
为了让类型转换可能发生于所有实参身上,我们需要一个non-member函数(条款24);为了令这个函数被自动具现化,我们需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是:令它成为一个friend。因此我们就这样做了。
当此
friend函数过于复杂时,令其调用类外的辅助函数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22template<typename T> class Rational; // 前向声明
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>&rhs);
{
return Rational<T>(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
template<typename T>
class Rational
{
public:
...
friend
const Rational<T>operator* (const Rational<T>& lhs,
const Rational<T>& rhs)
{
return doMultiply(lhs, rhs);
}
...
};作为一个
template,doMultiply当然不支持混合式乘法,但它其实也不需要。它只被operator*调用,而operator*支持混合式操作。
条款47:traits classes表现类型信息
STL迭代器分类:
input迭代器只能向前移动,一次一步,用户只能读取它所指的东西,而且只能读取一次,读完立即自动向前一步。程序库中的输入流迭代器
istream_iterator就属于这一类。output迭代器只能向前移动,一次一步,用户只能涂写它所指的东西,而且只能涂写一次,写完立即自动向前一步。程序库中的输出流迭代器
ostream_iterator就属于这一类。forward迭代器可以做上述两种迭代器做的事情,而且支持多次读写,读写完由用户自行决定是否前进。单向链表
slist提供的迭代器就属于这一类。bidirectional迭代器除了能做
forward迭代器做的事情,还可以向后移动。双向链表list以及set,mutilset,map,mutilmap提供的迭代器属于这一类。random access迭代器功能类似于内置指针,可以进行算数操作。
vector,deque和string提供的迭代器属于这一类。迭代器适配器
reverse迭代器insert迭代器back inserterfront insertergeneral inserter
stream迭代器move迭代器
1 | struct input_iterator_tag {}; |
这些struct之间的继承关系是有效的is-a关系:所有forward迭代器都是input迭代器,依此类推。
关于iterator_trait的实现技术(参考书籍P227-P232)
条款48:认识template元编程
- 如题,以后买本书再专门去学吧!