七.模板与泛型编程

本章旨在介绍一直支撑着所有基于template的编程

条款41:了解隐式接口和编译器多态

面向对象编程世界总是以显示接口和运行期多态解决问题。以下面这个class为例:

class Widget {
public:
      Widget();
      virtual ~Widget();
      virtual std::size_t size() const;
      virtual void normalize();
      void swap(Widget& other);      
      ...
};
void doProcessing(Widget& w)
{
      if(w.size() > 10 && w != someNastyWidget) {
            Widget temp(w);
            temp.normalize();
            temp.swap(w);
      }
}

我们可以这样分析:
- 由于w的类型被声明为Widget,所以w必须支持Widget接口。我们可以在源码中找出这个接口,这也是显示接口。
- 由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态。
Template及泛型编程的世界,与面向对象有根本的不同。在此世界中显示接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口和编译期多态移到前头,我们再探究将函数从函数模板时发生什么:

template<typename T>
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构造函数(用以建立temp)、不等比较。重要的是,这一组表达式便是T必须支持的一组隐式接口。
- 凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化,使这些调用得以成功,这样的具现化行为发生在编译器。“以不同的template参数具现化”会导致调用不同的函数,这就是所谓的编译器多态。你应该分清“运行期多态”和“编译器多态”之间的差异,因为它类似于“哪一个重载函数被调用”和“哪一个virtual函数该被绑定”之间的差异。
显示接口和隐式接口的差异,需要更多贴近的解释和说明。

1)

通常显示接口由函数的签名式(函数名称,参数类型,返回类型)构成。例如:

class Widget {
public:
      Widget();
      virtual ~Widget(); 
      virtual std::size_t size() const;
      virtual void normalize();
      void swap(Widget& other);
};

其接口由一个构造函数,析构函数、函数size,normalize,swap及其参数类型、返回类型、常量性构成。当然也包括编译器产生的copy构造函数和copy操作符。另外也可以包括typedefs.隐式接口就不同了,它并不基于函数签名式,而是由有效表达式组成,再次看看doProcessing template一开始的条件:

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

T的隐式接口看起来好像有些约束
- 它必须提供一个名为size的成员函数,该函数返回一个整数值。
- 它必须支持一个operator!=函数用来比较两个T对象。这里我们假设someNastyWidget的类型为T。
但感谢操作符重载带来的可能性,这两个约束都不需要满足。是的,T必须支持size成员函数,然而这个函数也可能从base class继承而得。这个成员函数不需返回一个整数值,甚至不需返回一个数值类型。它唯一需要的是返回一个类型为x的对象,而x对象加上一个int必须能够调用一个operator>,这个operator>不需要非得取得一个类型为x的参数不可,因为它也可以取得类型Y的参数,只要存在一个隐式转换能够将类型x的对象转换为类型y的对象!
隐式接口仅是有一组有效表达式构成,表达式自身可能看起来很复杂,但他们要求的约束条件一般而言相当直接又明确,例如以下条件:

if(w.size() > 10 && w != someNastyWidget) ...

关于函数size以及一系列的operator身上的约束条件,我们很难说太多,但确认整体表达式约束条件却很容易,if语句的条件式必须是个布尔表达式,所以无论涉及什么实际类型,都必须与bool兼容,这是T隐式接口的一部分,它要求的其他隐式接口:copy构造函数等都必须对T型对象有效。
加诸于template参数身上的隐式接口,就像加诸于class对象身上的显示接口一样真是,而且都在编译期完成检查。
请记住

1.classes和templates都支持接口和多态
2.对classes而言接口是显示的,以函数签名为中心。多态则是通过virtual函数发生于运行期。
3.对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译器行为。

条款42.了解typename的双重意义

一般情况下,typename和class是等价的,但是有时候你必须得使用typename。为了解其实际,我们必须先谈谈可以在template指涉的两种名称。假设我们有个template function。接受一个STL兼容容器为参数,容器内持有的对象可被赋值为ints,下面是实现方式:

template<typename T>
void print2nd(const C& container) //这不是有效的C++代码
{
      if(container.size() >= 2) {
      C::const_iterator iter(container.begin());
      ++iter;      
      int value = *iter;
      std::cout << value;
      }
}

代码中,template内出现的名称如果相依于某个template参数,称之为从属名称。如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称。其中value的类型是不依赖任何template参数的名称,这样的名称是非从属名称,嵌套从属名称有可能导致解析困难:

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

1)

有这样一种可能,const_iterator可能是C中的一个static变量,那么这段代码就变成了相乘动作。因此编译器考虑了这种情况,这也是上一段代码无效的原因,编译器不认为这是一个类型。为解决这个问题,只需要添加关键字typename:

template<typename C>
void print2d(const C& container)
{
      if(container.size() >= 2) {
            typename C::const_iterator iter(container.begin());
            ...
      }
}

这是一般性规则:在嵌套从属类型名称前一个位置添加关键字typename。

2)

typename必须作为嵌套从属类型名称的前缀词这一规则的例外是,typename不可以出现在base classes lists内的嵌套从属类型名称之前,也不可在member initialization list中作为base class修饰符。例如:

template<typename T>
class Derived: public Base<T>::Nested { //base class lists中不允许使用typename
public:
      explicit Derived(int x): Base<T>::Nested(x)  //member initialization list中不允许使用typename
      {
            typename Base<T>::Nested temp;
            ...
      }
      ...
};

3)

接下来是最后一个typename例子,那是你将在真实程序中看到的代表性例子,假设我们正在撰写一个function template,它接受一个迭代器,而我们打算为该迭代器指涉的对象做一份local复件temp,我们可以写:

template<typename IterT>
void workWithIterator(IterT iter)
{
      typename std::iterator_traits<IterT>::value_type temp(*iter)  //类型为IterT之对象所指之物的类型
      ...
}

如果IterT是vector::iterator,那么temp的类型就是int。由于std::iterator_traits::value_type是个嵌套从属类型名称(value_type被嵌套于iterator_traits之内而IterT是个template参数),所以我们必须在它之前放置typename。
请记住

1.声明template参数时,前缀关键字class和typename可互换。
2.请使用关键字typename标识嵌套从属类型名称;但不得在base class lists或成员初始化列表内以他作为bases class 修饰符。

条款43.学习处理模板化基类内的名称

假设我们需要撰写一个程序,它能够传送信息到若干不同的公司去。信息要不译成密码,要不就是未经加工的文字。如果编译期间我们有足够信息来决定哪一个信息传至哪一家公司,就可以采用基于template的解法:

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);
      ...
};
class MsgInfo { ... };
template<typename Company>
class MsgSender {
public:
      ...
      void sendClear(const MsgInfo& info)
      {
            std::string msg;
            Company c;
            c.sendCleartext(msg);
      }
      void sendSecret(const MsgInfo& info)
      { ... }
};
template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
      ...
      void sendClearMsg(const MsgInfo& info)
      {
            sendClear(info); //调用base class函数,这段码无法通过编译
      }
      ...
};

注意这个derived class的信息传送函数有一个不同的名称sendClearMsg,与其base class内的名称不同。这避免遮掩“继承而得的名称,也避免重新定义一个继承而得的non-virtual函数”,但是编译器却无法通过这样的设计,认为sendClear不存在。这是因为,当编译器遭遇class template LoggingMsgSender定义式时,并不知道它继承什么样的class。当然它继承的是MsgSender,但其中的Company是个template参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它是什么。这就导致了不知道class MsgSender看起来像什么——更明确的是没办法知道它是否有个sendClear函数。

1)

为了让问题更具现化,假设我们有个class CompanyZ坚持使用加密通讯:

class CompanyZ { //这个class不提供sendCleartext函数
public:
      ...
      void sendEncrypted(const std::string& msg);
      ...
};

针对这个CompanyZ产生一个MsgSender特化版:

template<> //不含参数表示这是一个特供版的MsgSender template,即模板全特化
class MsgSender<CompanyZ> {
public:
      ...
      void sendSecret(const MsgInfo& info)
      { ... }
};

现在让我们再次考虑derived class LoggingMsgSender:

typename<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
      ...
      void sendClearMsg(const MsgInfo& info)
      {
            sendClear(Info); //如果Company == CompanyZ,这个函数不存在。
      }
      ...
};

正如注释而言,当base class被指定为MsgSender时这段代码不合法,因为那个class并未提供sendClear函数!因为:C++知道base class templates有可能被特化,而那个特化版本可能不提供和一般性template相同的接口。因此拒绝在模板化基类内寻找继承而来的名称。为了重头来过,必须采取某种办法令C++不进入模板化基类观察的行为失效,有三个办法:

2)

第一是在base class函数调用动作之前加上"this->"

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
      ...
      void sendClearMsg(const MsgInfo& info)
      {
            this->sendClear(info);
      }
      ...
};

第二是使用using 声明式,条款33描述using声明式如何将“被掩盖的base class名称”带入一个derived class作用域内。我们可以这样写下sendClearMsg:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
      using MsgSender<Company>::sendClear; //告诉编译器请他假设sendClear位于base class内。
      ...
      void sendClearMsg(const MsgInfo& info)
      {
            ...
            sendClear(info);      
            ...
      };
      ...
};

第三是,明白指出被调用的函数位于base class内:

template<typename Company>
class LoggingMsgSender: public MsgSender<Company> {
public:
      ...
      void sendClearMsg(const MsgInfo& info)
      {
            ...
            MsgSender<Company>::sendClear(info);
            ...
      }
      ...
};

这是最不佳的解法,因为如果调用的是virtual函数,上述的明确资格修饰会关闭virtual绑定行为。
请记住

1.可在derived class templates内通过"this->"指涉base class templates内的成员名称,或藉由一个明白写出的"base class资格修饰符"完成。

条款44:将与参数无关的代码抽离templates

有时候使用template class看起来会使源码精简,但是产生的目标码反而臃肿肥胖,所以需要避免这样的二进制浮夸。使用的工具是共性与变性分析

1)

编写templates也是尽量避免重复,但是在non-template代码中,重复十分明确,然而template代码中重复是隐晦的:毕竟只有一份template源码,所以必须让自己去感受template被具现化多次时可能发生的重复。举个例子:

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

这个template接受一个类型参数T,除此之外还接受一个类型为size_t的参数,这是个非类型参数。现在考虑这些代码:

SquareMatrix<double, 5> sml;
...
sml.invert();
SquareMatrix<double, 10> sm2;
...
sm2.invert();

这会具现两份invert,这些函数并非完全相同,因为它们操作的矩阵大小不一样,但是除了常量,函数其他部分又是一样的。这是导致template代码膨胀的一个原因,下面进行修改:

template<typename T>
calss SquareMatrixBase { //与尺寸无关的base class,用于正方矩阵
protected:
      ...
      void invert(std::size_t matrixSize);      
      ...
};
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> { //private继承表示base class只是为了帮助derived class实现,不是表征is-a关系。
private:
      using SquareMatrixBase<T>::invert;  //避免遮掩base版的invert.
public:
      ...
      void invert() { this->invert(n); }  //避免模板化基类内的函数会被derived class掩盖。调用SquareMatrixBase<T>::invert(n)以降低template函数生成目标码的大小。
};

如你所见,带参数的invert位于base class SquareMatrixBase中,与class SquareMatrix不同的是,SquareMatrixBase只对“矩阵元素对象的类型”参数化,不对矩阵的尺寸参数化。因此对于某给定之元素对象类型,所有矩阵共享同一个SquareMatrixBase class。目前为止,还剩余一些问题没有解决:SquareMatrixBase::invert如何知道该操作什么数据?虽然它从参数中知道矩阵尺寸,但它如何知道哪个特定矩阵的数据在哪?这个只有derived class知道,但是derived class如何联系base class做逆运算动作。一个可能的做法是为SquareMatrixBase::invert添加另一个参数,也许是个指针,指向一块用来放置矩阵数据的内存起始点。我们唯一要做的是找出保存矩阵元素值的那块内存,我们可以对所有这样的函数添加一个额外参数,却得重复告诉SquareMatrixBase相同的信息,这样似乎不好。另外一个办法是令SquareMatrixBase存储一个指针,指向矩阵数值所在的内存。只要 它存储了数据,那么有可能存储矩阵尺寸:

template<typename T>
class SquareMatrixBase {
protected:
      SquareMatrixBase(std::size_t n, T* pMem): size(n), pData(Mem) { }  //存储矩阵大小和另一个指针,指向矩阵数值
      void setDataPtr(T* ptr) { pData = ptr; }
      ...
      private:
      std::size_t size;
      T* pData;
};

这允许derived class决定内存分配方式,某些实现版本也许会决定将矩阵数据存储在SquareMatrix对象内部:

template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
      SquareMatrix(): SquareMatrixBase<T>(n, data) { }
      ...
private:
      T data(n * n);
};

这种类型的对象不需要动态分配内存,但对象自身可能性非常大。另一种做法是把每一个矩阵的数据放进heap:

template<typename T, std::size_t>
class SquareMatrix: private SquareMatrixBase<T> {
public:
      SquareMatrix(): SquareMatrixBase<T>(n, 0), pData(new T[n * n])  //将base class的指针设为null,为矩阵内容分配内存,将指向该内存的指针存储,然后将它的副本交给base class
      {
            this->setDataPtr(pData.get());
      }
      ...
private:
      boost::scoped_array<T> pData;
}

最后得到的这个是最优化的版本。
请记住

1.Template生成多个class和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系
2.因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数
3.因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码

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

所谓智能指针是“行为像指针”的对象,并提供指针没有的机能。真实指针做得很好的一件事是,支持隐式转换。Derived class指针可以隐式转换为base class指针,“指向non-const对象”的指针可以转换为“指向const对象”:

class Top { ... }
class Middle: public Top { ... };
class Bottom: public Middle { ... };
Top* pt1 = new Middle;
Top* pt2 = new Bottom;
const Top* pc2 = pt1;

但是想在用户自定的智能指针中模拟上述转换,稍稍麻烦:

template<typename T>
class SmartPtr {
public:
      explicit SmartPtr (T* realPtr);
      ...
};
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top> pct2 = pt1;

但是,同一个template的不同具现体之间并不存咋什么与生俱来的故有关系,并没有base-derived关系,所以编译器将上述SmartPtr和SmartPtr等视为不同的类。如果需要获得明确的转换能力,我们必须明确地将他们写出来。
Template和泛型编程

1)

注意到,在上述智能指针实例中,每一个语句创建了一个新式智能指针对象,所以现在我们应该关注如何编写智能指针的构造函数,使其行为能够满足我们的转型需要。一个很关键的观察结果是,我们永远无法写出我们需要的所有构造函数,因为一个template可以被无限量具现化,以生成无限量函数。因此,我们需要的似乎不是为它实现一个构造函数,而是实现一个构造模板,这样的模板称为成员函数模板(member function templates),其作用是为class生成函数:

template<typename T>
class SmartPtr {
public:
      template<typename U>
      SmartPtr<const SmartPtr<U>& other>;
      ...
};

以上代码的意思是,对任何类型T和任何类型U,这里可以根据SmartPtr生成一个SmartPtr——因为SmartPtr有个构造函数接受一个SmartPtr参数。这一类构造函数根据对象u创建对象t,有时我们称之为泛化copy构造函数。上述转换未加explicit关键字,原因就是实现隐式转换是我们的目标,因此略去了。
完成声明之后,我们希望根据一个SmartPtr创建一个SmartPtr,却不希望根据一个SmartPtr创建一个SmartPtr,因此我们需要从某一方面对这一member template所创建的成员函数进行挑选或筛出。假设SmartPtr遵循auto_ptr和tr1::shared_ptr,也提供一个get成员函数,返回智能指针对象所持有的那个原始指针的副本,那么我们可以在“构造模板”实现代码约束转换行为,使它符合我们的期望:

template<typename T>
class SmartPtr {
public:
      template<typename U>
      SmartPtr(const SmartPtr<U>& const): heldPtr(other.get()) { ... } //初始化this的heldPtr指针
      T* get() const { return heldPtr; }
      ...
private:
      T* heldPtr; //Smart持有的内置原始指针
};

我们使用成员初值列来初始化类型为T的成员变量,并以类型为U的指针作为初值。这个行为只有当“存在某个隐式转换可以将U指针转为一个T指针”时才能通过编译,而那正是我们想要的。最终结果时SmartPtr现在有了一个泛化copy构造函数,这个构造函数只在其所获得的实参隶属适当类型时才通过编译。

2)

成员函数模板的效用不限于构造函数,它们还支持赋值操作。例如TR1的shared_ptr支持所有“来自兼容之内置指针、tr1::shared_ptr,auto_ptr和tr1::weak_ptr”的构造行为,以及来自上述所有指针的赋值操作。下面是TR1规范中关于tr1::shared_ptr的一份摘录:

template<class T>
class shared_ptr {
public:
      template<class Y>
      explicit shared_ptr(Y* p);
      template<class Y>
      shared_ptr(shared_ptr<Y> const& r);
      template<class Y>
      explicit shared_ptr(weak_ptr<Y> const& r);
      template<class Y>
      explicit shared_ptr(auto_ptr<Y>& r);
      template<class Y>
      shared_ptr& operator=(shared_ptr<Y> const& r);
      template<class Y>
      shared_ptr& operator=(auto_ptr<Y>& r);
      ...
};

下面是tr1::shared_ptr的一份定义摘要:

template<class T>
class shared_ptr {
public:
      shared_ptr(shared_ptr const& r);   //copy构造函数
      template<class Y>                 //泛化copy构造函数
      shared_ptr(shared_ptr<Y> const& r);
      shared_ptr& operator=(shared_ptr const& t); //copy assignment
      template<class Y>                          //泛化copy assignment
      shared_ptr& operator=(shared_ptr<Y> const& r);
      ...
};

上述说明,如果你想要控制copy构造的方方面面,你必须同时声明泛化copy构造函数和“正常的”copy构造函数,对于赋值操作同理。
请记住

1.请使用member function templates生成“可接受所有兼容类型”的函数
2.如果你声明member templates用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。

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

条款24讨论过为什么只有non-member函数才有能力在所有实参身上实施隐式类型转换,本条款在其基础上继续扩充讨论:

template<typename T>
class Rational {
public:
      Rational(const T& numerator = 0, const T& denominator = 1);  //条款20告诉你为什么参数以值传递
      const T numerator() const;                                  //条款28告诉你为什么返回值以pass by value传递
      const T denominator() const;                               //条款3告诉你为什么它们是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; //错误!

这告诉我们,模板化的Rational内的某些东西和non-template不同。事实的确如此,模板化之前,编译器知道我们尝试调用什么函数,但模板化之后编译器不知道我们想要调用哪个函数。取而代之的是,它们试图想出什么函数被名为operator的template具现化。它们知道它们应该可以具现化某个“名为operator并接受两个Rational参数”的函数,但为完成这一具现化行动,必须先算出T是什么。以oneHalf推导,operator的第一参数被声明为Rational,而传递给operator的第一参数是Rational,因此T一定是int。但传递的第二参数是int,编译器如何根据这个推算出T?你或许会期盼编译器使用Rational的non-explicit构造函数将2转换为Rational,进而将T推导为int,然后编译器拒绝这么做。因为在能够调用函数之前,必须知道那个函数存在,而为了知道它,必须先为相关的function template推导出参数类型,然后才可以将适当的函数具现化出来。然而template实参推导过程中并不考虑采纳通过构造函数而发生的隐式类型转换。

1)

只要利用一个事实,我们就可以缓和编译器在template实参推导方面受到的挑战:template class内的friend声明式可以指涉某个特定函数。那意味class Rational可以声明operator*是它的一个friend函数。

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

现在可以通过编译了,但却无法连接。针对这个问题,首先我们看看在Rational内声明operator的语法。在一个class template内,template名称可被用来作为“template和其参数”的简略表达方式,所以在Rational我们可以只写Rational而不必写Rational。本例中的operator被声明为接受并返回Rationals,如果它被声明如下,一样有效:

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

现在回头看我们的问题,混合式代码通过了编译,因为编译器知道我们要调用哪个函数。但那个函数只被声明于Rational内,并没有被定义出来,我们意图令此class外部的operator* template提供定义式。如果我们声明了一个函数,就有责任定义那个函数,既然我们美誉提供定义式,连接器当然找不到它!最简单的办法就是operator*函数本体合并至其声明式内:

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

现在一切正常!这项技术的趣味点在于,我们虽然使用friend,但却与friend的传统用途“访问class的non-public成分”毫不相干。为了让类型转换可能发生于所有实参身上,我们需要一个non-member函数;为了令这个函数被自动具现化,我们需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是:令它成为一个friend。此外,还需要考虑的是降低inline函数的代码量,在复杂的函数中,令friend函数调用辅助函数的做法是值得研究的,Rational是个template,意味着上述的辅助函数通常也是个template,所以:

template<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.denomerator());
}
template<typename T>
class Rational {
public:
      ...,
friend const Rational<T> operator*(cons Rational<T>& lhs, const Rational<T>& rhs)
{
      return doMultiply(lhs, rhs);
      ...
}

大功告成!
请记住

1.当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。

条款47.请使用traits classes表现类型信息

STL容器主要由以表现容器,迭代器和算法的template构成,但也覆盖若干工具性templates,其中一个名为advance,用来将某个迭代器移动某个给定距离:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d); //将迭代器向前移动d单位,若d<0,则向后移动

观念上advance只是做iter += d动作,但其实不可以全然那么实践,因为只有random access迭代器才支持+=操作,面对其他威力不那么强大的迭代器种类,advance必须反复施行++或--,共d次。

1)

首先回顾STL的5种迭代器分类,对应于它们支持的操作。
1.Input迭代器 只能向前移动,一次一步,客户只可读取不能涂写它们所指的东西,而且只能读取一次,它们模仿指向输入文件的阅读指针:C++程序库中的istream_iterators时这一分类的代表。
2.Output迭代器 与Input类似,但一切只为输出:它们只向前移动,一次一步,客户只可涂写它们所指的东西,而且只能读取一次。它们模仿指向输出文件的涂写指针:ostream_iterators是这一分类的代表。
3.Forward迭代器 这种迭代器可以做前述两种分类所能做的事,而且可以读写其所指物一次以上。这使得它们可施行于多次操作算法。STL并未提供单向linked list,但某些程序库有,而指入这种容器的迭代器就是属于forward迭代器。指入tr1 hashed容器的也可能是这一分类,取决于hashed容器的迭代器是单向还是双向实现版本。
4.Bidirectional迭代器 比上一个分类威力更大:除了可以向前移动,还可以向后移动。STL的list迭代器就属于这一分类,set,multiset和multimap的迭代器也都是这一分类。
5.Random access迭代器 这种迭代器比上一个分类威力更大的地方在于它可以执行“迭代器算术”,也就是可以在常量时间内向前或向后移动任意距离,类似于指针算数。因为random access正是以内置指针为榜样,而内置指针也可被当作random access迭代器使用,deque,vector和string提供的迭代器都是这一分类。
对于这五种分类,标准库提供专属的卷标结构加以确认:

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 { };

这些structs之间的继承关系是有效的is-a关系:所有forward迭代器都是input迭代器。以此类推。

2)

现在回到advance函数,我们已经知道STL迭代器有着不同的能力,实现advance的策略之一是采用“最低但最普及”的迭代器能力,以循环反复递增或递减迭代器。但random access迭代器支持迭代器算数运算,只需要常量时间,我们希望应用其优势:

template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
      if(iter is a random access iterator) {
            iter += d;
      }
      else {
            if(d >= 0) { while(d --) ++ iter; }
            else { while(d ++) -- iter;}
      }
}

这种做法首先必须判断iter是否为random access迭代器,也就是需要知道IterT是否为random access迭代器分类。这是traits能够完成的任务:允许在编译期间取得某些类型信息。traits并不是关键字或一个预先定义好的构件,它是一种技术,也是C++程序员共同遵守的协议。这个技术的要求之一是,它对内置类型和用户自定义类型的表现必须一样好。如果上述advance收到的实参是一个指针和一个int,上述advance仍然必须有效运作,那意味traits技术必须也能够施行于内置类型如指针身上。但值得注意的是,我们无法将信息嵌套于原始指针内。因此类型的traits信息必须位于类型自身之外。标准技术是把它放进一个template及其一或多个特化版本中。这样的templates在标注程序库中有若干个,其中针对迭代器被命名为iterator_traits:

template<typename IterT> //template用来处理迭代器分类的相关信息
struct iterator_traits;

虽然iterator_traits是个struct,但习惯上traits总被称为traits classes。iterator_traits的运作方式是,针对每一个类型IterT,在struct iterator_traits内一定声明某个typedef名为iterator_category,这个typedef用来确认IterT的迭代器分类。iterator_traits以两个部分实现上述所言,首先它要求每一个“用户自定义的迭代器类型”必须嵌套一个typedef,名为iterator_category,用来确认适当的卷表结构。例如deque的迭代器可随机访问,所以一个针对deque迭代器而设计的class看起来会是这样:

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

list的迭代器可双向行进,所以应该是这样:

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

至于iterator_traits只是响应iterator class的嵌套式typedef:

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

这对用户自定义类型行得通,但对指针行不通,因为指针不可能被嵌套typedef,iterator_traits的第二部分如下,用来解决指针问题,为了支持指针迭代器,iterator_traits特别针对指针类型提供一个偏特化版本。由于指针的行径与random access迭代器类似,所以iterator_traits为指针指定的迭代器类型是:

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

现在你应该知道如何设计并实现一个traits class了:
- 确认若干你希望将来可取的的类型相关信息。例如对迭代器而言,我们希望将来可取的分类。
- 为该信息选择一个名称(iterator_category)。
- 提供一个template和一个特化版本,内含你希望支持的类型相关信息。

3)

现在我们来实现之前advance的伪代码:

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

但这段代码存在编译问题。此外还有根本问题需要讨论,IterT类型在编译期间获知,所以iterator_traits::iterator_category也可在编译期间确定,但if语句却是在运行期间才会核定,为什么编译期间可完成的事延后到运行期才做呢?这会浪费时间,并带来代码膨胀。我们真正想要的是一个条件式判断“编译期核定成功”,恰巧C++提供重载完成这种行为。当重载某个函数f,你必须详细叙述各个重载件的参数类型。当调用f时,编译器便根据传来的实参选择最适当的重载件。编译器的态度是“如果这个重载件最匹配传递过来的实参,就调用这个f,以此类推”这正是一个针对类型而发生的编译器条件句,为了让advance的行为如我们所愿,我们需要做的是产生两版重载函数,内含advance的本质内容,但各自接受不同类型的iterator_category对象,不如将函数命名为doAdvance:

template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) //实现random access迭代器
{
      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迭代器。这是iterator_tag structs继承关系带来的一项红利:针对base class编写的代码用于derived class身上也行得通。advance函数规范说,如果面对的是random access和bidirectional迭代器,则接受正负距离,但如果是forward和input,移动负距离会导致不明确行为。有了这些onAdvance版本,advance需要做的只是调用它们并额外传递一个对象,后者必须带有明确的迭代器分类。于是编译器运用重载解析机制调用适当的代码:

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

现在我们可以总结如何使用一个traits class了:
- 建立一组重载函数或函数模板,彼此间的差异只在于各自的traits参数,令每个函数实现码与其接受之traits信息相应和。
- 建立一个控制函数,或函数模板,它调用上述函数并传递traits class所提供的信息。
请记住

1.Traits class使得“类型相关信息”在编译器可用,它们以template和“templates特化”完成实现
2.整合重载技术后,traits class有可能在编译期对类型执行if-else技术。

条款48.认识template元编程

Template metaprogramming是编写template-based C++程序并执行于编译期的过程。所谓模板元编程是以C++写成,执行于C++编译器内的程序。一旦TMP程序结束执行,其输出也就是从templates具现出来的若干C++源码,便会一如往常地被编译。模板元编程过程中,编译器必须确保对所有源码有效,即使有些代码在判断语句中不会被执行。条款47所提到的traits实际上就是一个TMP编程。

1)

TMP并没有真正的循环构件,所以循环效果藉由递归完成。下面看看TMP的阶乘运算示范如何通过“递归模板具现化”实现循环,以及如何在TMP中创建和使用变量:

template<unsigned n>
struct Factorial {
      enum { value = n * Factorial<n - 1>::value };
};
template<>
struct Factorial<0> {
      enum { value = 1 };
};
//使用
int main()
{
      std::cout << Factorial<5>::value;
      std::cout << Factorial<10>::value;
}   

循环发生在template具现化Factorial内部指涉另一个template具现体Factorial<n - 1>之时。和所有良好递归一样,我们需要一个特殊情况造成递归结束,这里的特殊情况是template特化体Factorial<0>,每个Factorial template具现体都是一个struct,每个struct都使用enum hack声明一个名为value的TMP变量,value用来保存当前计算所得的阶乘值。如果TMP拥有真正的循环构件,value应该在每次循环内获得更新。但由于TMP系以“递归模板具现化”取代循环,每个具现体有自己的一份value,而每个value尤其循环内的适当值。
请记住

1.Template metaprogramming可将工作由运行期移往编译器,因而得以实现早期错误诊断和更高的执行效率;

posted @ 2020-11-22 21:13  Viecgg  阅读(67)  评论(0)    收藏  举报