我根据自己的理解,对原文的精华部分进行了提炼,并在一些难以理解的地方加上了自己的“可能比较准确”的「翻译」。
Chapter 7 模版与泛型编程
Templates and Generic Programming
本章无法使你成为一个专家级的template程序员,但可以使你成为一个比较好的template程序员。本章也会给你必要信息,使你能够扩展你的template编程,到达你所渴望的境界。
条款41 : 了解隐式接口和编译器多态
在oop的世界里,我们总是以显式接口(explicit interfaces)和运行期多态(runtime polymorphism)解决问题。举个例子,给定这样(没啥意义)的class:
class Widget{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other); //见条款25
...
};
和这样的函数(也没啥意义):
void doProcessing(Widget& w)
{
if(w.size()>10 && w != someNastyWidget){
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
我们可以这么理解此函数内的 w :
由于w的类型是Widget,所以w必须支持Widget接口。我们可以在例如Widget.h中的源代码找出这个接口,看看是什么样子,此时称它为显式接口,也就是它在源码中明确可见。
由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态,也就是会在运行期间根据w的动态类型(条款37)调用匹配的函数。
但Template以及泛型编程的世界,与oop有根本上不同。在此世界中显式接口和运行期多态存在,但重要性降低。反倒是隐式接口(implicit interfaces)和编译器多态(compile-time polymorphism)相当重要。看看例子:
template<typename T> //doProcessing函数模版
void doProcessing(T& w)
{
if(w.size()>10 && w != someNastyWidget){
T temp(w);
temp.normalize();
temp.swap(w);
}
}
现在如何理解doProcessing内的w呢?
w必须支持哪种接口,系由template中对w的操作而定。本例看来w的类型T似乎必须支持size、normalize和swap成员函数、copy构造函数、不等比较。后面我们会知道这并非完全正确,但对目前而言足够真实。重要的是,这一组表达式便是T必须支持的一组隐式接口。
凡设计w的任何函数调用,例如operator>和operator!=,有可能造成template具现化,使这些调用成功。具现行为发生在编译期。”以不同的template参数具现化function templates”会导致调用不同函数,这便是所谓编译期多态。
你应该不陌生“运行期多态”和“编译期多态”之间的差异,因为它类似于“哪个重载函数该被调用(发生在编译期)”和“哪个virtual函数该被绑定(运行期)”之间的差异。而显式接口和隐式接口的差异比较新颖
通常显式接口由函数的签名式(函数名、参数类型、返回类型)构成
例如Widget class:
class Widget{
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
...
};
它的public接口由一个构造函数、一个析构函数、函数size、normalize、swap及其参数类型、返回类型、常量性构成。当然也包括编译器产生的拷贝构造函数和拷贝赋值(copy assignment)操作符。另外也可包括typedefs,甚至是你强制声明的public成员变量。
隐式接口则并不基于函数签名式,而是由有效表达式(valid expressions)组成。
再看看doProcessing template一开始的条件:
template<typename T>
void doProcessing(T& w)
{
if(w.size() > 10 && w != someNastyWidget){
....
T(w的类型)的隐式接口看来似乎有这些约束:
- 它必须提供一个名为size的成员函数,此函数返回一个int
- 它必须支持一个 operator!= 函数,用来比较两个T对象。这里我们假设someNastyWidget的类型为T
由于operator overloading带来的可能性,这两个约束都不需要满足。T必须支持size函数,但此函数可能从base class继承而来,这个成员函数不需返回一个int、甚至不需返回数值。由此来看,它甚至不需返回一个定义有operator>的类对象或其引用!它唯一需要做的是返回一个类型为X的对象,而X对象与一个int(也就是10)必须能调用一个operator>。这个operator>不需要非得取一个类型为X的参数不可,因它也可以取得类型Y的参数,只要存在一个隐式转换能将X对象转换为类型Y对象。
同理,T不需支持operator!=,因为以下这样也是可以的:
operator!= 接受一个类型为X的对象和一个类型为Y的对象,T可悲转换为X而someNastyWidget 的类型可被转换为Y,这样即可有效调用 operator!= 。
当我们第一次以这种方式思考隐式接口,会觉得头疼。隐式接口仅有一组有效表达式构成,表达式自身也许看起来复杂,但它们要求的约束条件一般相当直接而明确。例如以下表达式:
if(w.size() > 10 && w != someNastyWidget){
if语句的条件式必须是个bool表达式,所以无论涉及什么实际类型,无论“w.size() > 10 && someNastyWidget”导致什么,它都必须与bool兼容。这是template doProcessing加诸于其类型参数T的隐式接口的一部分。doProcessing要求的其他隐式接口:拷贝构造函数、normalize和swap也都必须对T型对象有效
请记住:
- classes和templates都支持interfaces和多态(polymorphism)
- 对classes而言接口是显示的,以函数签名为中心。多态则通过virtual函数发生于运行期
- 对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则通过template具现化和函数重载解析发生于编译期
条款42: 了解typename的双重意义
提问:以下template声明式中,class和typename有何不同?
template<class T> class Widget
template<typename T> class Widget
答案:完全相同。只不过某些程序员因为可少打几个字选择class,其他人喜欢typename,因为其暗示参数并非一定是一个class类型。
然而C++并不总把class和typename视为等价。有时你一定得使用typename。为了了解这种情况,我们必须先谈谈你可以在template内指涉(refer to)的两种名称。
假设有一个template function,接受一个STL兼容容器为参数,容器内持有的对象可被赋值为ints。再假设此函数仅打印其第二元素的值。这是个无聊的函数,下面是实践的一种方式:
template <typename C>
void print2nd(const C& container)
{ // 注意这不是有效的c++代码
if(container.size() >= 2){
C::const_iterator iter(container.begin());
++iter; // 将迭代器移往第二个元素
int value = *iter;
std::cout << value;
}
}
现在代码中强调两个local变量iter和value。iter的类型是C::const_iterator,实际怎样取决于template参数C。template内出现的名称若相依于某template参数,称之为从属名称。若从属名称在class内呈嵌套状,我们称它为嵌套从属名称。C ::const_iterator就是这样一个名称。实际上它还是个嵌套从属类型名称,也就是个嵌套从属名称且指涉某类型。
print2nd内的另一个local变量value,类型为int。int是一个并不倚赖任何template参数的名称。这样的名称是谓非从属名称。
嵌套从属名称可能会导致解析(parsing)困难。举个例子,我们将print2nd改成这样:
template <typename C>
void print2nd(const C& container)
{
C::const_iterator* x;
…
}
看起来我们好像声明x为一个local变量,它是个指针,指向一个C::const_iterator。但它之所以被那么认为,只因为我们“已经知道”C ::const_iterator是一个类型。但若它不是个类型呢?假设C有个static成员变量碰巧被命名为const_iterator,或x碰巧是个global变量名称?那样的话上述代码不再是声明一个local变量,而是一个相乘动作:
C::const_iterator乘以x
在我们知道C是什么之前,没有任何办法可以知道C::const_iterator是否为一个类型。当编译器开始解析template print2nd时,尚未确知C是什么。C++有一个规则可解析此歧义状态:若解析器在template中遭遇一个嵌套从属名称,它便假设这个名称不是个类型,除非你告诉它是。所以默认情况下嵌套从属名称不是类型。此规则有个例外,稍后提到。
现在看看print2nd的起始处:
template <typename C>
void print2nd(const C& container)
{
if(container.size() >= 2){
C::const_iterator iter(container.begin()); //这个名称被假设为非类型
...
现在清楚为啥这不是有效的C++代码了吧。iter声明式只有在C::const_iterator是个类型时才合理,当我们并没有告诉C++说它是,于是C++假设它不是。解决办法是紧邻它之前放置关键字typename即可:
template<typename C> //这是合法的C++代码
void print2nd(const C& container)
{
if(container.size() >= 2){
typename C::const_iterator iter(container.begin());
...
}
}
一般性规则很简单:任何时候你想在template中指涉一个嵌套从属类型名称,必须在它前面放置关键字typename。(很快会谈到一个例外)
typename仅用来验明嵌套从属类型名称;其它名称不该有它存在。例如下面这个函数模版,接受一个容器和一个“指向该容器”的迭代器:
template<typename C>
void f(const C& container, // 不允许使用typename
typename C::iterator iter); // 一定要使用
这一规则的例外是,typename不可出现在base classes list内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符。例如:
template<typename C>
class Derived: public Base<T>::Nested{ // base class list中
public: // 不允许typename
explicit Derived(int x)
: Base<T>::Nested(x) // mem.init.list中
{ // 不允许typename
typename Base<T>Nested temp; // 嵌套从属类型名称
...
}
...
};
让我们看看最后一个typename例子,那是你将在真实程序中看到的代表性例子。假设我们在撰写一个function template,它接受一个迭代器,而我们打算为该迭代器指涉的对象做一份local附件temp:
template<typename IterT>
void workWithIterator(IterT iter){
typename std::iterator_traits<IterT>::value_type temp(*iter);
...
}
std:: iterator_traits::value_type是标准traits class(条款47)的一种运用,相当于说“类型为IterT之对象所指之物的类型”。这个语句声明一个local变量temp,使用IterT对象所指物的相同类型,并将temp初始化为iter所指物。比如IterT是vector ::iterator,则temp的类型就是int。
如果你觉得这个名字太长了, 便想建立一个typedef。对于traits成员名称如value_type,普通的习惯是设定typedef名称用以代表某个traits成员名称,于是常常可看到类似这样的local typedef:
template<typename IterT>
void workWithIterator(IterT iter){
typedef typename std::iterator_traits<IterT>::value_type value_type;
value_type temp(*iter);
...
}
记得在typedef里加上必要的typename关键字!
值得注意的是,某些编译器接受的代码原本有的typename却被遗漏了;原本不该有typename的出现了;有的旧版本编译器直接拒绝typename。这意味在移植性方面会带给你头疼。
请记住:
- 声明template参数时,前缀关键字class和typename可互换
- 请使用typename标识嵌套从属类型名称;
条款43: 学习处理模版化基类内的名称
---持续更新中