effective c++之模板与泛型编程

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

本条款主要是两个概念,即什么是隐式接口,什么是编译期多态。面向对象编程世界总是以显式接口(explicit interfaces)和运行期多态(runtimepolymorphism)解决问题。例如:

class Widget { 
 public: 
  Widget(); 
  virtual ~Widget(); 
  virtual std::size_t size()const; 
  virtual void normalize(); 
  void swap(Widget& other);// see Item 25 

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

在Template及泛型编程的世界,显式接口和运行期多态的重要性降低。反倒是,隐式接口(implicitinterface)和编译期多态(compile-time polymorphism)更重要。

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

1)   函数内w支持哪种接口是由template中执行在w身上的操作来决定。这一组接口便是T必须支持这一组隐式接口。

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

编译期多态和运行期多态可以简单的理解成,一个是决定哪个重载函数该被调用(一般依据编译器的函数重命名规则来实现,发生在编译期),另一个是哪一个虚函数该被绑定(通过虚函数表和虚函数指针实现,发生在运行期,根据类中的虚函数指针来动态确定)。

显式接口就是一个完整的函数签名,明确指明了函数名称,参数类型,返回值。隐式接口由一系列有效表达式组成。比如这样一个表达式:

w.size()>10&&w!=someNastWidge

通过表达式来指明类型参数T具有哪些能力,这些便是隐式接口。

请记住:

1.   classtemplate都支持接口与多态;

2.   classes而言,接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期;

3.   template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。

 

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

Typename的第一重含义就是作为声明template类型参数,此时template<class T>和template <typename T>两者的意义完全相同。但是有时候一定要使用typename,即任何时候想要指涉一个嵌套从属类型名称,就必须紧临它前一个位置上放上关键字typename。

class Sample
{
public:
   typedef int myType;
// static constint myType = 0;
};
 
int main()
{
    Sample::myType *pb = new int(0); // 有的编译器会报错
}

有的编译器在这段代码上会产生歧义,因为它会默认的认为myType是一个静态成员,这样就变成了静态对象乘以pb。为了解决这个二义性,在前面加上typename,会强制编译器视为一个类型,而不是静态变量。但是使用typename也有两个特例,第一个是继承的时候:

class Derived:public Base::NestedClass{}; // 正确

class Derivedpublic typename Base::NextedClass(){}; // 错误

在继承某个类时,即使这个类名是嵌套从属类型也不需要使用typename,因为编译器不会认为它是个静态成员,因为继承类必然是个类型。

第二个是构造函数中的初始化成员列表:

Derived (int x):Base::NestedClass(x){}// 正确
Derived (int x):typename Base::NestedClass(x){} // 错误

此时编译器也不会将它视为静态对象,因为静态对象不支持初始化成员列表这种方式。

请记住:

1.   声明template参数时,前缀关键字classtypename可以互换;

2.   请使用关键字typename标识嵌套从属类型名称;但不得在base class lists或者member initialization list内使用typename


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

本条款其实就说了一个事:在继承模板化基类时,如果要使用基类中的成员,请通过”this->”或“基类资格修饰符”来完成,否则编译无法通过,因为C++具有“不进入模板化基类(templatized base classes)观察”的行为

假设现在编写一个发送信息到不同公司的程序,信息要么译成密码,要么就是原始文字,在编译期间来决定哪一家公司发送至哪一家公司,采用template手法:

classCompanyA{
    public:
        void sendCleartext(conststd::string& msg);
        void sendEncryted(conststd::string& msg);
       ……
    };
    class CompanyB{
    public:
        void sendCleartext(conststd::string& msg);
        void sendEncryted(conststd::string& msg);
       ……
    };
 
    class MsgInfo{……};//用来保存信息,以备将来产生信息
 
    template<typename Company>
    class MsgSender{
    public:
       ……//构造析构等函数
        void sendClear(const MsgInfo& info)
       {
           std::string msg;
           //根据info产生信息
           Company c;
           c.sendCleartext(msg);
       }
        void sendSecart(const MsgInfo& info)
       {……}
    };

上面这个做法行得通,但是如果要在每次送出信息时记录日志,可以派生出derived class:

template<typename Company>
    class LoggingMsgSender: publicMsgSender<Company>
    {
    public:
       ……//析构构造等
        void SendClearMsg(const MsgInfo& info)
       {
           //发送前的信息写到log
           sendClear(info);//调用base class函数,这段代码无法通过编译
           //传送后信息写到log
       }
    };

在派生类中,sendClearMsg和base class中的sendClear不同,这是个好设计,避免遮掩继承而得的名称,也避免了重新定义一个继承而得non-virtual函数。但是编译不能通过,因为编译器无法找到sendClear函数。因为当编译器遇到classtemplate LoggingMsgSender定义式时,不知道它继承什么样的class,因为MsgSender中的Company是个参数,在LoggingMsgSender被具体化之前,无法确切知道它是什么,自然而然就不知道classMsgSender是什么,也就不知道它是否有个sendClear函数。

我们必须有某种办法令C++“不进入templatized base class观察”的行为失效。有以下三种:

第一种:在base class函数调用动作之前加上“this->”

template<typename Company>
    class LoggingMsgSender: publicMsgSender<Company>
    {
    public:
       ……//析构构造等
        void SendClearMsg(const MsgInfo& info)
       {
           //发送前的信息写到log
           this->sendClear(info);
           //传送后信息写到log
       }
    };

第二种:使用using声明式

template<typename Company>
    class LoggingMsgSender: publicMsgSender<Company>
    {
    public:
       ……//析构构造等
       uinsg MsgSender<Company>::sendClear;//告诉编译器,假设sendClear位于base class内
        void SendClearMsg(const MsgInfo& info)
       {
           //发送前的信息写到log
           sendClear(info);//可以编译通过,假设sendClear将被继承
           //传送后信息写到log
       }
    };

第三种:明白指出被调用的函数位于base class内(使用了明确资格修饰符(explicitqualification),这将会关闭virtual绑定行为)

template<typename Company>
    class LoggingMsgSender: publicMsgSender<Company>
    {
    public:
       ……//析构构造等
        void SendClearMsg(const MsgInfo& info)
       {
           //发送前的信息写到log
           MsgSender<Company>::sendClear(info);//可以编译通过,假设sendClear被继承
            //传送后信息写到log
       }
    };

上面三种做法原理相同:对编译器承诺“base class template的任何特化版本都将支持其一般(泛化)版本所提供的接口”。

请记住:

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

 

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

emplate是节省时间和避免重复代码的一个奇妙方法,class template的成员函数只有在被使用时才被具现化。如果你不小心,使用templates可能导致代码膨胀(codebloat):其二进制代码带着重复(或几乎重复)的代码、数据、或两者。其结果可能源码看起来合身整齐,但目标码却不是那么回事。你需要知道如何避免这样的二进制浮夸。主要工具是:共性与变性分析。在non-template中,重复十分明确,然而在template中,重复是隐晦的:你必须训练自己去感受当template被具现化多次时可能发生的重复。举个例子,矩阵template,支持矩阵inversion运算:

template<typenameT, std::size_t n>
classSquareMatrix{
public:
    void invert();
};
template接受一个类型参数T,还有一个类型为size_t的非类型参数(non-typeparameter)。
SquareMatrix<double,5> sm1;
sm1.invert();
SquareMatrix<double,10> sm2;
sm2.invert();

这会具现两份invert。这些函数并非完全相同,但除了常量5和10,其他部分都相同,这是template引出代码膨胀的一个典型例子。下面是对SquareMatrix的第一次修改:

template<typenameT>                //与尺寸无关的base class
classSquareMatrixBase{
protected:
    void invert(std::size_t matrixSize);    //以给定的尺寸求逆矩阵
};
 
template<typenameT, std::size_t n>
classSquareMatrix: private SquareMatrixBase<T>{
private:
    using SquareMatrixBase<T>::invert;//避免遮掩base版的invert
public:
    void invert() {this->invert(n);}    //制造一个inline调用,用this->为了不被derived classes的函数名称掩盖
};

带参数的invert位于base class中。和SquareMatrix一样,也是个template,不同的是他只对“矩阵元素对象的类型”参数化,不对矩阵的尺寸参数化。因此对于给定的元素对象类型,所有矩阵共享同一个(也是唯一一个)SquareMatrixBaseclass。注意,SquareMatrixBaseSquareMatrix之间继承关系是private,这说明base class是为了帮助derived classes实现,两者不是is-a关系

请记住:

1. Template生成多个classes与多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。

2. 因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或者class成员变量替换template参数。

3. 因类型而造成的代码膨胀,也可以降低,做法是让带有完全相同二进制表述的具现类型共享实现码。

 

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

真实指针做得很好的一件事是,支持隐式转换。派生类指针可以隐式转换为基类指针,“指向non-const对象”的指针可以转换为“指向const对象”......等等。下面是可能发生于三层继承体系的一些转换:

class Top{...};
classMiddle:public Top{...};
classBottom:public Middle{...};
Top* pt1 = newMiddle;                 //将Middle*转换为Top*
Top* pt2 = newBottom;                 //将Bottom*转换为Top*
const Top* pct2= pt1;                 //将Top*转换为constTop*

  但如果想在用户自定义的智能指针中模拟上述转换,稍微有点麻烦。我们希望以下代码通过编译:

template<typenameT>
class SmartPtr
{
   public:
     explicit SmartPtr(T* realPtr);   //智能指针通常以内置(原始)指针完成初始化
     ...
};
 
SmartPt<Top>pt1 = SmartPtr<Middle>(new Middle);//将SmartPtr<Middle>转换为SmartPtr<Top>
SmartPt<Top>pt1 = SmartPtr<Bottom>(new Bottom);//将SmartPtr<Bottom>转换为SmartPtr<Top>
SmartPtr<constTop> pct2 = pt1;                 //将SmartPtr<Top>转化换为SmartPtr<const Top>

   但是,同一个template的不同具现体之间并不存在什么与生俱来的固有关系(这里指的是如果以带有基类-派生类关系的B,D两类型分别具现化某个模板,产生出来的两个具现体并不带有基类-派生类关系),所以编译器视SmartPtr<Middle>和SmartPtr<Top>为完全不同的的类,它们之间没有任何关系。为了获得我们所希望的SmartPtr类之间的转换能力,我们必须将它们明确地编写出来。假设日后添加了:

class BelowBottom:public Bottom{...};

   我们因此必须令SmartPtr<BelowBottom>对象得以生成SmartPtr<Top>对象,但我们当然不希望一再修改SmartPtr模板以满足此类需求。

   就原理而言,此例中我们需要的构造函数数量没有止尽,因为一个模板可以被无限量具现化,以致生成无限量函数。因此,似乎我们需要的不是为SmartPtr写一个构造函数,而是为它写一个构造函数模板。这样的模板是所谓的成员函数模板,其作用是为类生成函数:

 template<typename>
  class SmartPtr
  {
     public:
        template<typename U>
        SmartPtr(const SmartPtr<U>&other);   //成员模板,为了生成copy构造函数
        ...
  };

   以上代码的意思是,对任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T>——因为SmartPtr<T>有个构造函数接受一个SmartPtr<U>参数。这一类构造函数根据对象u创建对象t(例如根据SmartPtr<U>创建一个SmartPtr<T>),而u和v的类型是同一个模板的不同具现体,有时我们称之为泛化copy构造函数。

成员函数模板并不改变语言规则,而语言规则说,如果程序需要一个copy构造函数,你却没有声明它,编译器会为你暗自生成一个。在类内声明泛化copy构造函数(是个成员模板)并不会阻止编译器生成它们自己的copy构造函数(一个non-template)。所以如果你想控制copy的方方面面,你必须同时声明泛化copy构造函数和“正常的”copy构造函数。相同规则也使用于赋值操作。下面是tr1::shared_ptr的一份定义摘要:

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

请记住:

1.   使用成员函数模板生成“可接受所有兼容类型”的函数

2.   如果声明成员模板用于“泛化copy构造”或“泛化assignment操作”,还是需要声明正常的copy构造函数和copyassignment操作符。

 

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

条款 24中提到,如果所有参数都需要隐式类型转换,该函数应当声明为非成员函数。条款24是以Rational和operator*为例子展开的,本文把这个观点推广到类模板和函数模板。 但是在类模板中,需要所有参数隐式转换的函数应当声明为友元并定义在类模板中。模板化:

template<typenameT>
class Rational{
public:
  Rational(const T& numerator = 0, constT& denominator = 1);
  const T numerator() const;          
  const T denominator() const;       
};
 
template<typenameT>
constRational<T> operator*(const Rational<T>& lhs, constRational<T>& rhs){}

上述代码是条款24直接模板化的结果,看起来很完美但它是有问题的。比如我们有如下的调用:

Rational<int>oneHalf(1, 2);            // OK
Rational<int>result = oneHalf * 2;     // Error!

上述失败给我们的启示是,模板化的Rational内的某些东西似乎和其非模板版本不同。事实的确如此。传统的,编译器知道我们尝试调用什么函数(就是接受两个Rationals参数的那个operator*),但这里编译器不知道我们想要调用哪个函数。取而代之的是,它们试图想出什么函数被名为operator*的模板具现化出来。它们知道它们应该可以具现化某个“名为operator*并接受两个Rational<T>参数”的函数,但为完成这一具现化行动,必须先算出T是什么。问题是它们没这个能力。

以oneHalf进行推导,过程并不困难,operator*的第一个参数被声明为Rational<T>,而传递给operator*的第一个实参(oneHalf)的类型是Rational<int>,所以T一定是int。其他参数的推导则没有这么顺利。operator*的第二个参数被声明为Rational<int>,但传递给operator*的第二个实参(2)类型是int。编译器如何根据这个推算出T?你或许会期盼编译器使用Rational<int>的non-explicit构造函数将2转换为Rational<int>,进而将T推导为int。但它们不那么做,因为在模板实参推导过程中从不将隐式类型转换函数纳入考虑。绝不!这样的转换在函数调用过程中的确被使用,但在能够调用一个函数之前,首先必须知道那个函数存在。而为了知道它,必须先为相关的函数模板推导出参数类型(然后才可将适当的函数具现化出来)。然而模板实参推导过程中并不考虑采纳“通过构造函数而发生的”隐式类型转换。

只要利用一个事实,我们就可以缓和编译器在模板实参推导方面受到的挑战:模板类内的friend声明式可以指涉某个特定函数。那意味类Rational<T>可以声明operator*是它的一个friend函数。类模板并不依赖模板实参推导(后者只施行于函数模板身上),所以编译器总是能够在类Rational<T>具现化时得知T。因此,令Rational<int>类声明适当的operator*为其friend函数,可简化整个问题:

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

在Rational<T>中声明的friend没有添加模板参数T,这是一个简便写法,它完全等价于:friend const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs);

现在对operator*的混合式调用可以通过编译了,因为当对象oneHalf被声明为一个Rational<int>,Rational<int>于是被具现化出来,而作为过程的一部分,friend函数operator*(接受Rational<int>参数)也被自动化声明出来。后者身为一个函数而非函数模板,因此编译器可在调用它时使用隐式转换函数(例如Rationalnon-explicit构造函数),而这便是混合式调用之所以成功的原因。

此时编译可以通过,但是连接有问题,最简单的可行方法就是将operator*函数本体合并至其声明式内:

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

这样混合模式的调用result = oneHalf * 2终于可以编译、链接并且运行了:

·        为了对所有参数支持隐式类型转换,operator*需要声明为非成员函数;

·        为了让编译器推导出模板参数,operator*需要在类中声明;

·        在类中声明非成员函数的唯一办法便是声明为friend

·        声明的函数的同时我们有义务给出函数定义,所以在函数定义也应当放在friend声明中

如果operator*函数体变得很大,那么inline函数就不再合适了,这时我们可以让operator*调用外部的一个辅助函数:

template<typenameT> class Rational;
template<typenameT>
const Rational<T> doMultiply(constRational<T>& lhs, const Rational<T>& rhs);
 
template<typename T>
class Rational{
public:
    friendRational<T> operator*(const Rational<T>& lhs, constRational<T>& rhs){
        returndoMultiply(lhs, rhs);
    }
};

请记住

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

 

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

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

template<typenameIterT, typename DistT> 
voidadvance(IterT& iter, DistT d);  //将迭代器向前移动d个单位,d<0则向后移动。

STL里面的迭代器的分五类:

·        Input迭代器只能向前移动,一次一步,只读,且只能读取一次。模仿指向输入文件的阅读指针:istream_iterator是代表

·        Output迭代器类似,但只可涂写它们所指的东西,ostream_iterator是代表。它们只适合“一次性操作算法”one-passalgorithm。

·        forward迭代器,可以做前述两种分类所能做的每一件事,而且可以读或写其所指物一次以上。可施行于多次性操作算法。stl未提供单向linked_list,指入这种容器的迭代器就是属于forward迭代器。

·        bedirectional迭代器比上一个威力更大:除了可以向前移动,还可以向后移动。stl的list、set、multiset、map和multimap的迭代器都属于这一类。

·        random access迭代器比上一个更威力大,它可以执行迭代器算术,常量时间内可以向前或向后跳跃任意距离。内置指针也可以当random迭代器用。vector、deque、string提供的迭代器都是这一类。

我们真正希望的是以这种方式实现advance(伪代码如下):

template<typenameIterT, typename DistT>
voidadvance(IterT& iter, DistT d) {
       if (iter is a random_access_iterator)iter += d;  //针对random_access迭代器使用迭代器算术运算
       else {
              if (d >= 0) {
                     while (d--) ++iter;
              }else {
                     while (d++) --iter;
              }
       }
}

这种做法必须先判断iter是否为random_access迭代器。需要取得类型的某些信息。那就是traits让你进行的事:它们允许在编译期间取得某些类型信息。traits并不是c++关键字或一个预先定义好的构件:它们是一种技术,也是一个c++程序员共同遵守的协议。这个技术的要求之一是,它对内置类型和用户自定义类型的表现必须一样好

标准模板库是把traits信息放到模板中,其中针对迭代器的被命名为iterator_traits,它是一个结构体:

template<typenameIterT>

structiterator_traits;

它的工作原理是:针对每一个类型IterT,在结构体中声明某个typedef名为iterator_category,这个typedef用于确认IterT的迭代器分类。它包括两部分实现:

·        对于自定义类型,用户必须在类模板中声明一个typedef名为iterator_category,比如对双端队列和列表的模板类:

template<typename ...>
class deque{
       public:
              iterator{
                     public:
                           typedefrandom_access_iterator_tag iterator_category;  //双端队列的迭代器可以随机访问
       ...
};
template<typename ...>
class list{
       public:
              iterator{
                     public:
                           typedefbidirectional_access_iterator_tag iterator_category;  //列表的迭代器可以随机访问
       ...
};

在iterator_traits中,获取迭代器的类型信息:

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

对于内置类型的指针而言,iterator_traits提供一个偏特化版本:

template<typename IterT>
struct iterator_traits<IterT*>{
       typedefrandom_access_iterator_tag iterator_category;  //内置类型的指针类型与random access迭代器类似
       ...
};

到此为止,你应该知道了如何设计和实现一个 traits class:

·        识别你想让它可用的关于类型的一些信息(例如,对于iterators(迭代器)来说,就是它们的 iterator category(迭代器种类))。

·        选择一个名字标识这个信息(例如,iterator_category)。

·        提供一个 template(模板)和一系列specializations(特化)(例如,iterator_traits),它们包含你要支持的类型的信息。

iterator_traits ——实际上是 std::iterator_traits,因为它是C++ 标准库的一部分——我们就可以改善我们的 advance 伪代码:

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

这个虽然看起来有点希望,但它不是我们想要的。在某种状态下,它会导致编译问题,但是我们到条款 48 再来研究它,现在,有一个更基础的问题要讨论。IterT的类型在编译期间是已知的,所以iterator_traits<IterT>::iterator_category可以在编译期间被确定。但是if 语句还是要到运行时才能被求值。为什么要到运行时才做我们在编译期间就能做的事情呢?它浪费了时间(严格意义上的),而且使我们的执行码膨胀。然而我们真正想要的是一个条件式(也就是一个if...else语句)判断“编译期核定成功”之类型。恰巧C++有一个取得这种行为的办法,那就是重载。所以为了让advance的行为如我们所期望,我们需要做的是产生两版重载函数,内含advance的本质内容,但各自接受不同类型的iterator_category对象:

template<typename IterT, typename DistT>
voiddoAdvance(IterT& iter, DistT d, random_access_iterator_tag) {
       iter += d;
}
 
template<typename IterT, typename DistT>
voiddoAdcance(IterT& iter, DistT d, bidirectionl_iterator_tag) {
       if(d >= 0) {
              while (d--) ++iter;
       }else {
              while (d++) --iter;
       }
}
 
template<typename IterT, typename DistT>
voiddoAdvance(IterT& iter, DistT d, input_iterator_tag) {
       if (d < 0) {
              throw out_of_range("Negativedistance");
       }
       while (d--) ++iter;
}

有了上面定义的这些重载的函数,那么advance需要做的只是调用它们并额外传递一个对象:

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

现在能够概述如何使用一个 traits class 了:

·        创建一套重载的 "worker"functions(函数)或者 function templates(函数模板)(例如,doAdvance),它们在一个traits parameter(形参)上不同。与传递的 traits 信息一致地实现每一个函数。

·        创建一个 "master"function(函数)或者 function templates(函数模板)(例如,advance)调用这些workers,传递通过一个 traits class 提供的信息。

请记住:

1.   traits classes 使关于类型的信息在编译期间可用。它们使用 templates(模板)和 template specializations(模板特化)实现。

2.   结合 overloading(重载),traits classes 使得执行编译期类型 if...else 检验成为可能。

 

条款48:认识template元编程

Template metaprogramming(TMPS,模板元编程)是编写template-basedC++程序并执行编译期的过程。它本质上是以C++写成、执行于C++编译器内的程序。一旦TMP程序结束执行,其输出,也就是从templates具现出来的若干C++源码,便会一如往常地被编译。TMP有两个伟大的效力:第一,它让某些事情更容易。如果没有它,那些事情将是困难,甚至不可能。第二,欲与TMP执行于C++编译期,因此可将工作从运行期转移到编译期。这导致一个结果是,某些错误原本通常在运行期才能检测到,现在可在编译期找出。这样带来的结果是很美好的。因为使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存要求。然而它也会带来另一个令人感到不愉快的结果是,编译时间变长了。

为了再次认识下事物在TMP中如何运作,来看下循环。TMP没有真正循环,循环由递归(recursion)完成。TMP递归甚至不是正常的递归,因为TMP递归不涉及递归函数调用,而是涉及递归模板化(recursivetemplate instantiation)。TMP的起手程序是在编译期计算阶乘。TMP的阶乘运输示范如何通过递归模板具体化实现循环,以及如何在TMP中创建和使用变量:

template<unsignedn>
    struct Factorial{
        enum{value=n*Factorial<n-1>::value};
    };
    template<>
    struct Factorial<0>{ //特殊情况,Factorial<0>的值是1
        enum {value=1};
    };

有了这个template metaprogram,只要指涉Factorial::value就可以得到n阶乘值。循环发生在template具体化Factorial内部指涉另一个template具体化Factorial之时。特殊情况的template特化版本Factorial<0>是递归的结束。

用Factorial示范TMP就像用hello world示范编程语言一样。为了领悟TMP之所以值得学习,就要先对它能够达成什么目标有一个比较好的理解。下面举三个例子:

  • 确保量度单位正确。使用TMP就可以确保在编译期所有量度单位的组合都正确。
  • 优化矩阵运算。
  • 可以生成客户定制之设计模式(custom design pattern)实现品。使用policy-based design之TMP-based技术,有可能产生一些templates用来表述独立的设计项(所谓policies),然后可以任意结合它们,导致模式实现品带着客户定制的行为。

请记住:

1.   Template metaprogrammingTMP,模板元编程)可将工作由运行期移到编译期,因而得以实现早期错误侦测和更高的执行效率。

2.   TMP可被用来生成“基于政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值