我根据自己的理解,对原文的精华部分进行了提炼,并在一些难以理解的地方加上了自己的“可能比较准确”的「翻译」。

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

如果你觉得这个名字太长了, 便想建立一个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: 学习处理模版化基类内的名称

---持续更新中

内容来源于网络如有侵权请私信删除
你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!

相关课程

3766 8.82元 9.8元 9折