zoukankan      html  css  js  c++  java
  • 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不会成为主流,但是对某些程序员,特别是程序库开发人员,几乎肯定会成为他们的主要粮食。

  • 相关阅读:
    webpack(二)
    webpack(一)
    初探Vue SSR(1)
    Node版本管理控制器n
    Gitlab用户在组中有五种权限:Guest、Reporter、Developer、Master、Owner
    微信小程序分享参数传递
    关于vue-cli3.*搭建项目遇到问题整理
    请求头出现provisional headers are shown 和 vue-cli 3.x配置跨域代理
    HDU6409 没有兄弟的舞会
    HDU6446 Tree and Permutation
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/9388783.html
Copyright © 2011-2022 走看看