Effective C++: 07模板与泛型编程

C++ template机制自身是一部完整的图灵机(Turing-complete):它可以被用来计算任何可计算的值。于是导出了模板元编程(TMP, template metaprogramming),创造出“在C++编译器内执行并于编译完成时停止执行”的程序。

 

41:了解隐式接口和编译期多态

         所谓显式接口(explicit interface),是指在源码中明确可见的接口,显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。而运行时多态,就是指继承和virtual带来的动态绑定机制。

         在Templates及泛型编程的世界里,隐式接口和编译期多态更重要一些。比如下面的模板定义:

template<typename T>
void doProcessing(T& w)
{
  if (w.size() > 10 && w != someNastyWidget) {
     T temp(w);
     temp.normalize();
     temp.swap(w);
  }
}

 w必须支持哪一种接口,是由template中执行于w身上的操作来决定。本例看来w的类型T好像必须支持size,normalize和swap成员函数、copy构造函数、不等比较。这一组表达式便是T必须支持的一组隐式接口(implicit interface)。

         凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化(instantiated),使这些调用得以成功。这样的具现行为发生在编译期。“以不同的template参数具现化function templates”会导致调用不同的函数,这便是所谓的编译期多态。

 

42:了解typename的双重意义

         以下template声明式中,class和typename的意义是完全相同的,但更推荐typename:

template<class T> class Widget;                 // uses "class"
template<typename T> class Widget;              // uses "typename"        

但是,某种情况下必须使用typename。首先看下面的函数:

template<typename C>  
void print2nd(const C& container) 
{                                               // this is not valid C++!
  if (container.size() >= 2) {
     C::const_iterator iter(container.begin()); // get iterator to 1st element
     ++iter;                                    // move iter to 2nd element
     int value = *iter;                         // copy that element to an int
     std::cout << value;                        // print the int
  }
}

          在上面的函数模板中,iter的类型是C::const_iterator,而实际是什么必须取决于template参数C。如果 template内出现的名称相依于某个template参数,称之为从属名称(dependent names)。如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称(nested dependent name )。C::const_iterator就是这样一个名称。实际上它还是个嵌套从属类型名称(nested dendent type name ),也就是个嵌套从属名称并且指涉某类型。

另一个local变量value,其类型是int。int是一个并不倚赖任何template参数的名称。这样的名称是谓非从属名称(non-dependent names)。

嵌套从属名称有可能导致解析困难。比如:

template<typename C>
void print2nd(const C& container)
{
  C::const_iterator * x;
  ...
}

 看上去好像是我们将 x 声明为一个指向 C::const_iterator 的局部变量。但编译器不这么认为,比如如果 C 有一个静态数据成员碰巧就叫做 const_iterator 呢?而且 x 碰巧是一个全局变量的名字呢?在这种情况下,上面的代码就不是声明一个 局部变量,而成为 C::const_iterator 乘以 x!

直到 C 成为已知之前,没有任何办法知道 C::const_iterator 到底是不是一个类型。C++ 有一条规则解决这个歧义:如果解析器在一个模板中遇到一个嵌套从属名字,它假定那个名字不是一个类型,除非你明确告诉它。所以,一开始的print2nd中的语句并不合法:

C::const_iterator iter(container.begin());   

 iter 的 声明仅在 C::const_iterator 是一个类型时才有意义,但是我们没有告诉 C++ 它是,所以C++ 就假定它不是。要想纠正这个错误,必须明确指出C::const_iterator 是一个类型:这就必须使用typename:

template<typename C>                           // this is valid C++
void print2nd(const C& container)
{
  if (container.size() >= 2) {
    typename C::const_iterator iter(container.begin());
    ...
  }
}

 

一般性规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename。

typename只被用来验明嵌套从属类型名称,其他名称不该有它存在。例如:

template<typename C>                   // typename allowed (as is "class")
void f(const C& container,             // typename not allowed
     typename C::iterator iter);       // typename required

 上述的C并不是嵌套从属类型名称(它并非嵌套于任何“取决于template参数”的东西内),所以声明container时并不需要以typename为前导,但C::iterator是个嵌套从属类型名称,所以必须以typename为前导。

 

上面的规则有个例外:typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在成员初始化列表中作为base class修饰符。例如:

template<typename T>
class Derived: public Base<T>::Nested { // base class list: typename not allowed
public:                                 
  explicit Derived(int x)
  : Base<T>::Nested(x)          // base class identifier in mem
  {                                // init. list: typename not allowed

    typename Base<T>::Nested temp;      // use of nested dependent type
    ...                                 // name not in a base class list or
  }                                     // as a base class identifier in a
  ...                                   // mem. init. list: typename required
};

 

 来看最后一个例子:

template<typename IterT>
void workWithIterator(IterT iter)
{
  typename std::iterator_traits<IterT>::value_type temp(*iter);
  ...
}

 上面的函数以迭代器为参数,函数第一条语句的意思是为该迭代器所指向的对象创建一个副本。std::iterator_traits<IterT>::value_type 就是表示IterT所指对象的类型。比如如果 IterT 是 vector<int>::iterator,则temp 就是 int 类型。

std::iterator_traits<IterT>::value_type是一个嵌套从属类型名称(value_type 嵌套在 iterator_traits<IterT>内部,而且 IterT 是一个 模板参数),所以必须在它之前放置 typename。

 

43:学习处理模板化基类内的名称

看一下下面的代码,它表示需要将信息发到若干不同的公司,信息要么是明文,要么是密文:

class CompanyA {
public:
  ...
  void sendCleartext(const std::string& msg);
  void sendEncrypted(const std::string& msg);
  ...
};

class CompanyB {
public:
  ...
  void sendCleartext(const std::string& msg);
  void sendEncrypted(const std::string& msg);
  ...
};
...                                     // classes for other companies

class MsgInfo { ... };                  // class for holding information
                                        // used to create a message
template<typename Company>
class MsgSender {
public:
  ...                                   // ctors, dtor, etc.
  void sendClear(const MsgInfo& info)
  {
    std::string msg;
    create msg from info;

    Company c;
    c.sendCleartext(msg);
  }

  void sendSecret(const MsgInfo& info)   // similar to sendClear, except
  { ... }                                // calls c.sendEncrypted
};

 现在假设有了新的需求,需要在每次发送明文是记录日志。此时可以通过继承实现:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
  ...                                    // ctors, dtor, etc.
  void sendClearMsg(const MsgInfo& info)
  {
    //write "before sending" info to the log;

    sendClear(info);                     // call base class function;
                                         // this code will not compile!
    //write "after sending" info to the log;
  }
  ...
};

 上面的代码无法通过编译,编译器会抱怨sendClear不存在。尽管在base class中确实定义了sendClear。

问题在于,当编译器见到LoggingMsgSender这个模板类的定义时,并不知道它继承什么样的类。当然它继承的是MsgSender<Company>,但其中的Company是个template参数,只有在具现化LoggingMsgSender才知道Company是什么,而如果不知道Company是什么,就无法知道MsgSender<Company>是否有个sendClear函数。

比如,现在有个公司只能发送密文:

class CompanyZ {                             // this class offers no
public:                                      // sendCleartext function
  ...
  void sendEncrypted(const std::string& msg);
  ...
};

 此时一般性的MsgSender对CompanyZ就不合适了,必须产生一个MsgSender特化版:

template<>                                 // a total specialization of
class MsgSender<CompanyZ> {                // MsgSender; the same as the
public:                                    // general template, except
  ...                                      // sendCleartext is omitted
  void sendSecret(const MsgInfo& info)
  { ... }
};

 现在有个MsgSender针对CompanyZ的全特化版本,再次考虑LoggingMsgSender的实现,当base class指定为MsgSender<CompanyZ>时,这段代码不合法,因为该base class没有sendClear函数。

这就是编译失败的原因,编译器知道base class模板类可能被特化,而特化版本可能不提供一般性模板相同的接口,所以它才拒绝在模板化基类(本例中的MsgSender<Company>)内寻找继承而来的名称(本例中的SendClear)。

有三种办法可以明确指出使编译器进入模板基类中寻找名称:第一是在base class的函数调用时加上this->:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
  ...
  void sendClearMsg(const MsgInfo& info)
  {
    //write "before sending" info to the log;
    this->sendClear(info);    // okay, assumes that sendClear will be inherited
    //write "after sending" info to the log;
  }
  ...
};

 

第二是使用using声明式:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
  ...   
  using MsgSender<Company>::sendClear;   // tell compilers to assume
                                 // that sendClear is in the base class                                  
  void sendClearMsg(const MsgInfo& info)
  {
    ...
    sendClear(info);   // okay, assumes that sendClear will be inherited
    ...   
  }
};

 

第三是明确指出函数位于base class内:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
  ...
  void sendClearMsg(const MsgInfo& info)
  {
    ...
    MsgSender<Company>::sendClear(info);      // okay, assumes that
    ...                                       // sendClear will be inherited
  }                                           
};

 第三种方法有个缺点,就是当被调用的virtual函数时,会关闭virtual绑定行为。

 

从名字可见性的观点来看,上面方法都做了同样的事情:它向编译器保证任何后继的 base class template(基类模板)的特化版本都将支持通用模板提供的接口。

但是如果保证被证实不成立,真相将在后继的编译过程中暴露。例如,如果后面的源代码中包含这些:

LoggingMsgSender<CompanyZ> zMsgSender;
MsgInfo msgData;
...                                          // put info in msgData
zMsgSender.sendClearMsg(msgData);            // error! won't compile

 对 sendClearMsg 的调用将不能编译,因为在此刻,编译器知道 base class是MsgSender<CompanyZ>,也知道那个类没有提供 sendClear函数。

 

从根本上说,问题就是编译器是早些(当 derived class template definitions(派生类模板定义)被解析的时候)诊断对 base class members(基类成员)的非法引用,还是晚些时候(当那些 templates(模板)被特定的 template arguments(模板参数)实例化的时候)再进行。C++ 的方针是宁愿早诊断,而这就是为什么当那些 classes(类)被从 templates(模板)实例化的时候,它假装不知道 base classes(基类)的内容。

 

 44:将与参数无关的代码抽离template

为了避免重复代码,当编写某个普通函数,其中某些部分的实现码和另一个函数的实现码实质相同,此时,抽出两个函数的共同部分,把它们放进第三个函数中,然后令原先两个函数调用这个新函数。同样道理,如果你正在编写某个class,而其中某些部分和另一个class的某些部分相同,可以把共同部分搬移到新class去,然后使用继承或复合,令原先的classes取用这共同特性。而原classes的互异部分仍然留在原位置不动。

编写templates时,也可以做同样的优化,以相同的方式避免重复。在non-template代码中,重复十分明确;然而在template代码中,重复是隐晦的。

举个例子,为固定尺寸的正方矩阵编写一个支持逆矩阵运算的template:

template<typename T, std::size_t n> 
class SquareMatrix {
public:
  ...
  void invert();
};

SquareMatrix<double, 5> sm1;
sm1.invert();                  // call SquareMatrix<double, 5>::invert

SquareMatrix<double, 10> sm2;
sm2.invert();                  // call SquareMatrix<double, 10>::invert

 sm1.invert和sm2.invert函数调用会具现化两份invert。这些函数并非完完全全相同,但除了常量5和10,两个函数的其他部分完全相同。这是template引出代码膨胀的一个典型例子。

首先想到为它们建立一个带数值参数的函数,然后以5和10来调用这个带参数的函数,而不重复代码:

template<typename T>                   // size-independent base class for
class SquareMatrixBase {               // square matrices
protected:
  ...
  void invert(std::size_t matrixSize); // invert matrix of the given size
  ...
};

template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
private:
  using SquareMatrixBase<T>::invert; 
public:
  ...
  void invert() { this->invert(n); } 
};

 SquareMatrixBase也是个template,不同的是它只对“矩阵元素对象的类型”参数化,不对矩阵的尺寸参数化。因此对于某给定的元素对象类型,所有矩阵共享同一个SquareMatrixBase class。它们也将因此共享这唯一一个class内的invert函数。

 

 45:运用成员函数模板接受所有兼容类型

         智能指针的行为像指针,并提供真实指针没有的机制保证资源自动回收。真实指针支持隐式转换:Derived class指针可以隐式转换为base class指针,指向non-const对象的指针可以转换为指向const对象的指针等,比如:

class Top { ... };
class Middle: public Top { ... };
class Bottom: public Middle { ... };
Top *pt1 = new Middle;                   // convert Middle* => Top*
Top *pt2 = new Bottom;                   // convert Bottom* => Top*
const Top *pct2 = pt1;                   // convert Top* => const Top*

 

         如果想在自定义的智能指针中模拟上述转换:

template<typename T>
class SmartPtr {
public:                             // smart pointers are typically
  explicit SmartPtr(T *realPtr);    // initialized by built-in pointers
  ...
};

SmartPtr<Top> pt1 =                 // convert SmartPtr<Middle> =>
SmartPtr<Middle>(new Middle);       // SmartPtr<Top>

SmartPtr<Top> pt2 =                 // convert SmartPtr<Bottom> =>
SmartPtr<Bottom>(new Bottom);       // SmartPtr<Top>

SmartPtr<const Top> pct2 = pt1;     // convert SmartPtr<Top> =>
                                    // SmartPtr<const Top>

          上面的代码是错误的。同一个template的不同具现体之间并不存在什么先天的固有关系,也就是说:如果以带有base-derived关系的S, D两类型分别具现化某个template,产生出来的两个具现体并不带有base-derived关系。

         所以,为了获得SmartPtr classes之间的转换能力,必须明确写出来。上面的语句时创建智能指针对象,所以考虑构造函数的实现。但是不可能写出所有需要的构造函数,上面的代码中,根据一个SmartPtr<Middle> 或 SmartPtr<Bottom> 构造出一个 SmartPtr<Top>,但是如果将来这个继承体系被扩充,还需要重新定义一个构造函数,这是不现实的。

         我们需要的不是为SmartPtr写一个构造函数,而是写一个构造模板,这就是所谓的member function templates:

template<typename T>
class SmartPtr {
public:
  template<typename U>                       // member template
  SmartPtr(const SmartPtr<U>& other);        // for a "generalized
  ...                                        // copy constructor"
};

          这个构造模板的意思是:对任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T>,有时又称之为泛化copy构造函数。

         上面的声明还不够:我们希望根据一个SmartPtr<Bottom> 创建一个 SmartPtr<Top>,但是不需要能够从一个 SmartPtr<Top> 创建一个 SmartPtr<Bottom>,因此必须在某方面对这个member template所创建的成员函数群进行筛除。可以这样做:

template<typename T>
class SmartPtr {
public:
  template<typename U>
  SmartPtr(const SmartPtr<U>& other)         // initialize this held ptr
  : heldPtr(other.get()) { ... }             // with other's held ptr

  T* get() const { return heldPtr; }
  ...
private:                                     // built-in pointer held
  T *heldPtr;                                // by the SmartPtr
};

          这里使用U*初始化T*,这个行为只有在:存在某个隐式转换可将U*指针转换为T*指针时才能通过编译。

         成员函数模板的作用并不仅限于构造函数,它还可以作用于赋值操作副。

 

         如果类没有定义copy构造函数,编译器会自动生成一个。在类内声明泛化copy构造函数并不会阻止编译器生成它们自己的copy构造函数。这个规则也适用于赋值操作。

 

 

46:需要类型转换时请为模板定义非成员函数

         条款24讨论过为什么只有非成员函数才能“在所有实参身上实施隐式类型转换”,该条款以Rational的operator*函数为例。现在将Rational和operator*模板化,代码如下:

template<typename T>
class Rational {
public:
  Rational(const T& numerator = 0,     // see Item 20 for why params
           const T& denominator = 1);  // are now passed by reference

  const T numerator() const;           // see Item 28 for why return
  const T denominator() const;         // values are still passed by value,
  ...                                  // Item 3 for why they're const
};

template<typename T>
const Rational<T> operator*(const Rational<T>& lhs,
                            const Rational<T>& rhs)
{ ... }

          像条款24一样,我们希望支持混合式算术运算,所以我们希望下面的代码通过编译:

Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2;   // error! won't compile

          上面的代码并不能通过编译。在条款24中,编译器知道我们尝试调用什么函数,但是这里编译器却不知道。虽然这里有个函数模板是operator*,它接受两个Rational<T>参数,但是在模板实参推导过程中,从不考虑隐式类型转换。

         这里的调用中,operator*的参数一个是Rational<int>,另一个是int类型,没有这样的模板函数可以具现化出这样的函数,所以编译失败。

 

         可以利用以下规则解决这个问题:模板类中的friend声明可以指涉某个特定函数:

template<typename T>
class Rational {
public:
  ...
friend                                              
  const Rational operator*(const Rational& lhs,     
                           const Rational& rhs);    
};

template<typename T>                                // define operator*
const Rational<T> operator*(const Rational<T>& lhs, // functions
                            const Rational<T>& rhs)
{ ... }

          此时,当对象oneHalf被声明为一个Rational<int>时,类Rational<int>也就被具现化出来了,那么friend函数operator*也就自动被声明出来。后者作为一个函数而不是函数模板,编译器可以再调用它时使用隐式转换。

此时,代码虽然能够通过编译,但是却无法链接成功。当声明一个Rational<int>时,该类被具现化出来,但其中的friend声明也仅仅是个声明,还没有找到定义,也就是函数体并未具现化(尽管在Rational外部提供了该friend的定义,但是那是一个函数模板,在没有遇到参数匹配的函数调用之前,不会具现化,这里的调用时oneHalf*2,参数不匹配,所以不会具现化这个模板)。

最简单的解决办法就是讲定义体放在Rational内:

template<typename T>
class Rational {
public:
  ...

friend const Rational operator*(const Rational& lhs, const Rational& rhs)
{
  return Rational(lhs.numerator() * rhs.numerator(),       // same impl
                  lhs.denominator() * rhs.denominator());  // as in
}                                                          // Item 24
};

 现在可以通过编译、连接,并能执行了。

 

这个技术的趣味点是:虽然使用了friend,但是与friend的传统用途“访问class中非public部分”毫不相干:为了让类型转换可以作用于所有实参上,需要一个non-member函数(条款24);为了这个函数能自动具现化,需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是令他成为一个friend。

        

         条款30中说,class内定义的函数都自动成为inline,包括像operator*这样的friend函数。可以这样将inline声明所带来的冲击最小化:让operator*不做任何事情,而是调用一个定义与class外部的辅助函数(本例中意义不大,因为operator*已经是个单行函数,但对复杂函数而言,这样做也许有意义)。

         Rational是个模板,意味着那个辅助函数通常也是个模板,所以代码如下:

template<typename T> class Rational; 

template<typename T>                                    
const Rational<T> doMultiply(const Rational<T>& lhs,    
                             const Rational<T>& rhs);   

template<typename T>
class Rational {
public:
  ...
friend
  const Rational<T> operator*(const Rational<T>& lhs,
                              const Rational<T>& rhs)   // Have friend
  { return doMultiply(lhs, rhs); }                      // call helper
  ...
};

template<typename T>                                      // define
const Rational<T> doMultiply(const Rational<T>& lhs,      // helper
                             const Rational<T>& rhs)      // template in
{                                                         // header file,
  return Rational<T>(lhs.numerator() * rhs.numerator(),   // if necessary
                     lhs.denominator() * rhs.denominator());
}

          因为定义在Rational内部的operator*需要调用doMultiply函数模板,所以,需要在Rational之前声明doMultiply,而doMultiply原型中,又用到了Rational模板,所以在它之前又需要声明Rational。

         作为一个template,doMultiply当然不支持混合式乘法,但它其实不需要。他只是被operator*调用,而operator*支持混合式乘法,也就是调用operator*时,参数已经完成了隐式转换。

  

47:使用traits classes表现类型信息

         STL中有一个名为advance的template,它用于将某个迭代器移动指定距离:

template<typename IterT, typename DistT> 
void advance(IterT& iter, DistT d); 

          只有随机访问迭代器支持+=操作,所以其他类型的迭代器,在advance中只能反复执行++或--操作,供d次。

        

STL共有5中迭代器分类,对应于它们支持的操作:input迭代器只能向前移动,一次一步,只能读取它们所指的东西,而且只能读取一次,istream_iterators就是这种迭代器;output迭代器类似,只能向前移动,一次一步,只能写它们所指的东西,且只能写一次,ostream_iterators是这类迭代器;forward迭代器,可以做前述两种类型迭代器所能做的每件事,且可以读或写所指物一次以上,单向链表类型的容器的迭代器就属于forward迭代器;bidirectional迭代器除了可以向前移动,也可以向后移动。set,map等的迭代器属于这一类;最强大的迭代器是random access迭代器,它更强大的地方在于可以执行迭代器算术,也就是常量时间内向前或向后跳跃任意距离。vector、deque,string的迭代器属于这一类,指针也被当做random access迭代器。

对于这5中迭代器,C++标准库提供了卷标结构用以区分:

struct input_iterator_tag {};
struct output_iterator_tag {};
struct forward_iterator_tag: public input_iterator_tag {};
struct bidirectional_iterator_tag: public forward_iterator_tag {};
struct random_access_iterator_tag: public bidirectional_iterator_tag {};

 

 回到advance函数,它的伪代码应该是下面这个样子:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
  if (iter is a random access iterator) {
     iter += d;                                      // use iterator arithmetic
  }                                                  // for random access iters
  else {
    if (d >= 0) { while (d--) ++iter; }              // use iterative calls to
    else { while (d++) --iter; }                     // ++ or -- for other
  }                                                  // iterator categories
}

 这就需要判断iter是否为random access迭代器,也就是我们需要取得类型信息。这就是traits的作用,它允许你在编译期获得某些类型信息。traits是一种技术,也是一种约定,它的要求之一是需要对内置类型和用户自定义类型要表现的一样好。也就是说,advance收到的实参如果是一个指针,则advance也需要能够运作。

因为traits需要支持内置类型,所以traits信息必须位于类型自身之外。比如,在标准库中,针对迭代器的traits就命名为iterator_traits:

template<typename IterT> struct iterator_traits; 

 习惯上,traits总是被实现为structs。iterator_traits的运作方式是,针对每一个类型IterT,在struct iterator_traits<IterT>内一定声明某个typedef名为iterator_category。这个typedef用来确认IterT的迭代器分类。

iterator_traits以两部分实现上述所言。首先它要求每一个用户自定义的迭代器类型必须嵌套一个typedef,名为iterator_category。比如deque和list的迭代器定义:

template < ... > 
class deque {
public:
  class iterator {
  public:
    typedef random_access_iterator_tag iterator_category;
    ...
  };
  ...
};

template < ... >
class list {
public:
  class iterator {
  public:
    typedef bidirectional_iterator_tag iterator_category;
    ...
  };
  ...
};

 而在iterator_traits这个模板类中,只是简单的鹦鹉学舌:

// the iterator_category for type IterT is whatever IterT says it is;
template<typename IterT>
struct iterator_traits {
  typedef typename IterT::iterator_category iterator_category;
  ...
};

 上面的做法对于指针是行不通的,所以有一个偏特化版本:

template<typename IterT>
struct iterator_traits<IterT*>
{
  typedef random_access_iterator_tag iterator_category;
  ...
};

 现在可以对advance实践先前的伪代码:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
  if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
     typeid(std::random_access_iterator_tag))
  ...
}

 实际上这个实现是有编译问题的,而且IterT类型在编译期间就可知了,所以iterator_traits<IterT>::iterator_category也可以在编译期间确定。但if语句却是在运行期才能确定。

我们真正需要的是在编译期间就能判断类型是否相同,在C++中,函数的重载就是在编译期间确定类型的例子。所以,可以使用重载技术实现advance:

template<typename IterT, typename DistT> 
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag)
{
  iter += d;
}

template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag)       
{
  if (d >= 0) { while (d--) ++iter; }
  else { while (d++) --iter; }
}

template<typename IterT, typename DistT>             
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag)
{
  if (d < 0 ) {
     throw std::out_of_range("Negative distance");    
  }
  while (d--) ++iter;
}

 forward_iterator_tag 继承自 input_iterator_tag,所以上面的doAdvance的input_iterator_tag版本也能处理forward迭代器。

实现了这些doAdvance重载版本之后,advance的代码如下:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
  doAdvance( 
    iter, d, 
    typename std::iterator_traits<IterT>::iterator_category() 
  );             
} 

  

TR1导入了许多新的traits classes用以提供类型信息,包括is_fundamental<T>判断T是否为内置类型,is_array<T>判断T是否是数组,以及is_base_of<T1, T2>判断T1和T2是否相同,或者是否T1是T2的base class。

 

 48:认识template元编程

         所谓template metaprogram(TMP,模板元程序)是以C++写成、执行于C++编译期内的程序。一旦TMP程序结束执行,其输出,也就是从templates具现出来的若干C++源码,便会一如往常地被编译。

TMP有两个效力。第一,它让某些事情更容易。如果没有它,那些事情将是困难的,甚至不可能的。第二,由于template metaprograms执行于C++编译期,因此可将工作从运行期转移到编译期。这导致的一个结果是,某些错误原本通常在运行期才能侦测到,现在可在编译期找出来。另一个结果是,使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。然而将工作从运行期移转至编译期的另一个结果是,编译时间变长了。

条款47中提到的advance的traits实现,就是TMP的例子。在条款47中,还提到了一种运行期判断类型的实现:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
  if (typeid(typename std::iterator_traits<IterT>::iterator_category) ==
      typeid(std::random_access_iterator_tag)) {
     iter += d;                                     // use iterator arithmetic
  }                                                 // for random access iters
  else {
    if (d >= 0) { while (d--) ++iter; }             // use iterative calls to
    else { while (d++) --iter; }                    // ++ or -- for other
  }                                                 // iterator categories
}

 这个实现不但效率低,而且还会存在编译问题,比如针对std::list<int>::iterator iter的具现化代码如下:

void advance(std::list<int>::iterator& iter, int d)
{
  if (typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category) ==
      typeid(std::random_access_iterator_tag)) {
    iter += d;                                        // error!
  }
  else {
    if (d >= 0) { while (d--) ++iter; }
    else { while (d++) --iter; }
  }
}

 问题出在iter+=d上,这条语句尝试在list<int>::iterator上使用其不支持的+=操作。尽管在运行期间永远不会执行+=操作,但是编译期间编译器必须确保所有源码有效,某条语句执行类型不支持某种操作肯定会引起编译错误。

 

TMP已被证明是个图灵完备的,也就是说TMP可以计算任何事物,使用TMP可以声明变量,执行循环,编写调用函数等。比如循环,TMP中没有真正的循环构件,循环效果是通过递归实现的。以计算阶乘为例:

template<unsigned n>                 
struct Factorial {
  enum { value = n * Factorial<n-1>::value };
};

template<>                           // special case: the value of
struct Factorial<0> {                // Factorial<0> is 1
  enum { value = 1 };
};

 可以这样使用Factorial:

int main()
{
  std::cout << Factorial<5>::value;            // prints 120
  std::cout << Factorial<10>::value;           // prints 3628800
}

 以上只是一个TMP最简单的例子,TMP还有很多更酷的玩法。

 

TMP的缺点是语法不够直观,或许TMP不会成为主流,但是对某些程序员,特别是程序库开发人员,几乎肯定会成为他们的主要粮食。

posted @ 2018-07-30 08:35  gqtc  阅读(345)  评论(0编辑  收藏  举报