第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 inserter
front inserter
general inserter
stream
迭代器move
迭代器
1 | struct input_iterator_tag {}; |
这些struct
之间的继承关系是有效的is-a
关系:所有forward
迭代器都是input
迭代器,依此类推。
关于iterator_trait
的实现技术(参考书籍P227-P232
)
条款48
:认识template
元编程
- 如题,以后买本书再专门去学吧!