美文网首页Effective C++
【Effective C++(7)】模板与泛型编程

【Effective C++(7)】模板与泛型编程

作者: downdemo | 来源:发表于2018-01-08 15:16 被阅读20次

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

    • OOP总是通过显式接口和运行期多态解决问题,如函数doProcessing内的w
      • 类型被声明为Widget,所以必须支持Widget接口,可以在源码中找到此接口(例如在Widget的.h文件中),称其为显式接口
      • w对虚函数的调用表现出运行期多态
    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 != someNasyWidget) {
            Widget temp(w);
            temp.normalize();
            temp.swap(w);
        }
    }
    
    • 在Template和泛型编程的世界中,显式接口和运行期多态仍然存在,但隐式接口和编译期多态更重要,此时的doProcessing内的w
      • 必须支持哪种接口由template中执行于w身上的操作来决定,看起来T必须支持size,normalize和swap函数,copying构造函数,不等比较,但这并不完全这确。通过表达式推断出来的函数接口便是T必须支持的一组隐式接口
      • 凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template实例化使调用成功,此行为发生在编译期。以不同模板参数实例化函数模板会导致调用不同的函数,这就是编译期多态
    template<typename T>
    void doProcessing(T& w)
    {
        if (w.size() > 10 && w != someNasyWidget) {
            T temp(w);
            temp.normalize();
            temp.swap(w);
        }
    }
    
    • 编译期多态和运行期多态的区别类似于重载函数的调用(编译期)和虚函数的动态绑定(运行期),显式接口和隐式接口的区别比较新颖,通常显式接口由函数的签名式(函数名称、参数类型、返回值)构成,如下面的Widget class,public接口由一个构造函数、一个析构函数、函数size、normalize、swap及其参数类型、返回类型、常量性(constness)构成,还包括编译器产生的copy构造函数和copy assignment操作符,另外也可以包括typedef或者其他public成员变量
    class Widget {
    public:
        Widget();
        virtual ~Widget();
        virtual std::size_t size() const;
        virtual void normalize();
        void swap(Widget& other);
        ...
    };
    
    • 隐式接口则是由有效表达式组成,再看看doProcessing一开始的条件,看起来似乎必须提供一个返回整数值的size函数,支持一个operator!=函数用来比较两个T对象,但并非如此。T必须支持size成员函数,但这个函数可能从基类继承,它不需要返回一个整数值,甚至不需要返回一个数值类型,甚至不需要返回一个定义有operator>的类型。它唯一要做的就是返回一个类型为X的对象,而X对象加上一个int必须能够调用一个operator>。operator>不一定要取一个类型为X的参数,它也可以取类型为Y的参数,只要存在一个X到Y的隐式转换。同理T不需要支持operator!=,只要T可被转换成X而someNasyWidget可被转换成Y(当然,不考虑operator&&被重载)
    template<typename T>
    void doProcessing(T& w)
    {
        if (w.size() > 10 && w != someNasyWidget) {
           ...
    

    42 了解typename的双重意义

    • template声明式中class和typename的意义完全相同
    template<class T> class Widget;
    template<typename T> class Widget;
    
    • 但有时一定要用typename。假设有一个函数模板,接受一个STL兼容容器为参数,容器内对象可被赋值为int
    // 打印容器第二个元素,该代码不合法,后续说明原因
    template<typename C>
    void print2nd(const C& container)
    {
        if(container.size() >= 2)  {
            C::const_iterator iter(container.begin());
            ++iter;
            int value = *iter;
            std::cout << value;
        }
    }
    
    • iter的类型是 C::const_iterator,实际的类型取决于参数C。template内出现的名称如果相依于某个template参数,称之为从属名称,如果从属名称在 class 内呈嵌套状,我们称它为嵌套从属名称,如C::const_iterator,实际上它还指涉某种类型,所以还是嵌套从属类型名称。另一个local变量value是int类型,并不依赖任何模板参数,称为非从属名称。嵌套从属名称可能会导致解析困难
    template<typename C>
    void print2nd(const C& container)
    {
        C::const_iterator* x;
        ...
    }
    
    • 在知道C::const_iterator是类型的前提下,上面的代码看起来没问题,但如果C有个static成员变量碰巧命名为 const_iterator,或x碰巧是一个global变量名称,那么里面的 * 可能不是指针,而是乘号。在知道C是什么之前,无法知道C::const_iterator是否为类型,解析器在template中遭遇一个嵌套从属名称时,它便假设其不是类型,因此缺省情况下嵌套从属名称不是类型。现在可知,iter只有在C::const_iterator是个类型时才合理,而我们没有指出,所以C++就假设它不是。因此如果想在template中指涉一个嵌套从属类型名称,就必须在紧邻它的前一个位置放置关键字typename
    // 合法的代码
    template<typename C>
    void print2nd(const C& container)
    {
        if (container.size() >= 2)  {
            typename C::const_iterator iter(container.begin());
            ...
    
    • 有个例外,typename不能出现在基类列表内的嵌套从属名称之前,也不能在成员初值列中作为base class修饰符
    template<typename T>
    class Derived: public Base<T>::Neted  { // base class list中不允许typename
    public:
        explicit Derived(int x) : Base<T>::Nested(x) // 成员初值列中不允许typename
        {
            typename Base<T>::Nested temp; // 除了前两种情况嵌套从属名称都得加上typename
            ...
        }
        ...
    };
    
    • 嵌套从属名称很长时,若要使用typedef,在typename前面加上typedef
    template<typename IterT>
    void workWithIterator(IterT iter)
    {
        typename std::iterator_trains<IterT>::value_type temp(*iter);
        ...
    }
    // 加上typedef
    template<typename IterT>
    void workWithIterator<IterT iter)
    {
        typedef typename std::iterator_trains<IterT>::value_type value_type;
        value_type temp(*iter);
        ...
    }
    

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

    • 继承模板基类调用基类函数时,派生类拒绝在基类内寻找继承而来的名称,因为模板参数到后来才确定,派生类此前无法知道基类是什么,更无法明确知道基类是否有该函数存在
    template<typename T>
    class A {
    public:
        void f() {
            ...
        }
        ...
    private:
        ...
    };
    
    template<typename T>
    class B : public A<T> {
    public:
        void useF(){
            f();  // 调用基类函数,编译错误,编译器无法在模板化基类中查找f
        }
        ...
    private:
        ...
    };
    
    • 有三个解决方法,第一是在base class函数调用动作之前加上this->,第二是使用using声明式,第三是明确指出被调用的函数位于base class内,第三种做法的缺点是如果调用的是虚函数会关闭动态绑定
    // 在base class函数调用动作之前加上this->
    template<typename T>
    class B : public A<T> {
    public:
        void useF(){
            this->f();
        }
        ...
    };
    
    // 使用using声明式
    template<typename T>
    class B : public A<T> {
    public:
        using A<T>::f;
        void useF(){
            f();
        }
        ...
    };
    
    // 明确指出被调用的函数位于base class内,这种做法的缺点是如果调用的是虚函数会关闭动态绑定
    template<typename T>
    class B : public A<T> {
    public:
        void useF() {
            A<T>::f();
        }
        ...
    };
    

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

    • 模板是节省时间和避免代码重复的一个很好的办法,但如果不小心可能会导致代码膨胀:其二进制带着重复的代码,数据,或两者,结果可能是源码看起来合身而整齐,但目标码却不是那么回事,避免这样的二进制浮夸的主要工具是共性与变性分析
    • 编写某个函数,发现某些部分的实现和另一个函数相同,抽出共同部分放进第三个函数,然后令原先两个函数调用这个新函数。同理,如果是class,把共同部分搬移到新class,然后使用继承或复合,令原先的 class取用这共同特性,原class的互异部分留在原位。但在模板代码中重复是隐晦的,毕竟只存在一份模板源码,所以必须训练自己感受当模板被多次实例化时可能发生的重复
    // A是一个n * n矩阵,元素是类型为T的对象
    template <typename T, int n>
    class A {
    public:
        void f();
    };
    
    // 下面会实例化两份f,引起代码膨胀
    A<double, 5> a;
    a.f();
    A<double, 10> b;
    b.f();
    
    • 本能来说会建立一个带数值参数的函数来避免重复
    template <typename T>
    class A {
    protected:
        void f(int n);
    };
    
    template <typename T, int n>
    class B : private A<T> { // 基类只是为了帮助派生类实现而非is-a关系,所以用private继承
    private:
        using A<T>::f;
    public:
        void f() { this->f(n); }
    };
    
    • 但还有些问题,派生类如何告诉基类数据在哪,A::f如何知道该操作什么数据?方法是令A贮存一个指针指向n所在的内存
    template <typename T>
    class A {
    protected:
        A(int n, T* pMem) : number(n), pData(pMem) {}
        void setDataPtr(T* ptr) { pData = ptr; }
        ...
    private:
        int number;
        T* pData;
    };
    
    • 这允许派生类决定内存分布方式。某些实现版本也会将数据存在B对象内部
    template <typename T, int n>
    class B : private A<T> {
    public:
        B() : A<T>(n, data) {}
        ...
    private:
        T data[n*n];
    };
    
    • 这种类型的对象不需要动态分配内存,但对象自身可能非常大。还有一种做法是通过new分配内存,把每一个矩阵的数据放进heap
    template <typename T, int n>
    class B : private A<T> {
    public:
        B() : A<T>(n, 0), pData(new T[n*n])
        { this->setDataPtr(pData.get()); }
        ...
    private:
        boost::scoped_array<T> pData;
    };
    

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

    • 真实指针可以进行的隐式转换
    class Top { ... }; 
    class Middle : public Top { ... }; 
    class Bottom : public Middle { ... }; 
    Top* pt1 = new Middle;        // 将Middle*转换成Top* 
    Top* pt2 = new Bottom;        // 将Bottom*转换成Top* 
    const Top* pct2 = pt1;        // 将Top*转换成const Top*
    
    • 在自定的智能指针中模拟上述行为,我们希望以下代码通过编译
    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;
    
    • 同一个模板的不同实例之间不存在固有关系,继承关系的两类型分别实例化某个模板,产生出的两个实例并不带有继承关系,所以编译器视SmartPtr<Middle>和SmartPtr<Top>为完全不同的classes,为了获得转换能力必须将它们明确地编写出来,我们需要的不是写一个构造函数,而是写一个构造模板,称为member function template,其作用是为class生成函数
    template<typename T>
    class SmartPtr {
    public:
        template<typename U>   // member template
        SmartPtr(const SmartPtr<U>& other);  // 为了生成copy构造函数
        ...
    };
    
    • 以上代码的意思是,任何类型T和任何类型U,可以根据SmarPtr<U>生成一个SmartPtr<T>,这一类构造函数通过对象u创建对象t,我们称之为泛化copy构造函数。为了效仿隐式转换,它并未声明为explicit。完成声明后必须从某方面对member template所创建的成员函数群进行筛除,我们希望根据一个SmartPtr<bottom>创建一个SmartPtr<top>,却不希望反过来,也不希望根据一个SmartPtr<double>创建一个SmartPtr<int>,因为现实中并没有将double*转换成为int*的隐式转换。假设SmartPtr和shared_ptr一样也提供一个get成员函数返回智能指针原本持有的指针,由此可以在构造模板实现代码中约束转换行为
    template<typename T>
    class SmartPtr {
    public: 
        template<typename U>
        SmartPtr(const SmartPtr<U>& other)
        : heldPtr(other.get()) { ... }
        T* get() const { return heldPtr; }
        ...
    private:
        T* heldPtr; // 该智能指针持有的原始指针
    };
    
    • 成员初值列中,以类型为U*的指针初始化类型为T*成员变量,这个行为只有当“存在某个隐式转化可将一个U*指针转换为一个T*指针”时才能通过编译,这正是我们的目的。现在SmartPtr<T>有了一个泛化copy构造函数,这个构造函数只在其所获得的实参隶属适当(兼容)类型时才通过编译
    • member template并不改变语言规则,程序仍需要一个copy构造函数,如果没声明,编译器会生成一个。声明一个泛化copy构造函数并不阻止编译器生成它们自己的copy构造函数,所以要控制copy构造函数的方方面面,必须同时声明泛化copy构造函数和正常的copy构造函数,赋值操作符同理
    template<class T>
    class shared_ptr {
    public:
        shared_ptr(shared_ptr const& r); // copy构造函数
    
        template<class Y>
        shared_ptr(shared_ptr<Y> const& other); // 泛化copy构造函数
    
        shared_ptr& operator= (shared_ptr const& r); // copy assignment
    
        template<class Y>
        shared_ptr& operator= (shared_ptr<Y> const& r); // 泛化copy assignment
    };
    

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

    • 条款24提到过为什么non-member函数才有能力“在所有实参身上实施隐式类型转换”,本条款将Rational class模板化
    template<typename T>
    class Rational {
    public:
        Rational(const T& numerator = 0, const T& denominator = 1);
        const T numerator() const;
        const T denominator() 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; // 编译错误
    
    • 条款24内,编译器知道调用operator*,但这里编译器想知道operator*的template实例化什么函数,必须先算出T是什么,以oneHalf进行推导,第一个实参类型是Rational<int>,所以T一定是int,但第二个实参类型是int,编译器无法由此推算出T。template实参推导过程中不会考虑隐式转换,所以编译器也不会使用构造函数将2转换为Rational<int>进而将T推导为int
    • 解决方法是,template class内的friend声明式可以指涉某个特定函数,class Rational<T>可以声明operator*是它的一个friend函数,class template并不依赖template实参演绎,所以编译器总是能在class Rational实例化时得知T
    template<typename T> 
    class Rational { 
    public: 
        ...
        // class template内,template名称就是template和其参数的简写
        // 因此不必写Rational<T>,简写节省时间而且代码更干净
        friend
        const Rational operator* (const Rational& lhs, const Rational& rhs);
    };
    
    template<typename T> 
    const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
    { ... }
    
    • 现在对operator*的混合式调用可以通过编译了,当对象oneHalf被声明为一个Rational<int>,class Rational<int>被实例化,friends函数operator*(接受Rational<int>参数)也跟着被自动声明出来。friend函数身为一个函数而非函数模板,因此编译器可在调用它时使用隐式转换函数。不过这段代码虽然能通过编译,却无法连接,函数声明于Rational内并没有被定义,通过外部的operator* template提供定义式是没用的,它相当于自己声明的一个函数,没有提供定义式,连接器当然找不到它,解决方法就是将operator*函数本体合并至声明式中
    template<typename T>
    class Rational {
    public:
        ...
        // Rational<T>都简写为Rational
        friend
        const Rational operator* (const Rational& lhs, const Rational& rhs)
        {
            return Rational(lhs.numerator() * rhs.numerator(),
                lhs.denominator() * rhs.denominator());
        }
    };
    
    • 这里使用friend不同于传统用途“访问class的non-public成分”。为了让类型转换可能发生在所有实参上,需要non-member函数,为了令这个函数被自动实例化,需要将它声明在class内部,而在class内部声明non-member函数的唯一办法就是friend,因此我们这样做了
    • 定义于class内的函数都暗自成为inline,包括operator*这样的friend函数。将inline声明的开销最小化的做法是令operator*不做任何事情,只调用一个定义于class外部的辅助函数。Rational是个模板意味着这个辅助函数通常也是一个模板,模板定义应当放进头文件内
    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.denominator()); 
    }
    
    template <typename T>
    class Rational {
    public:
        ...
        friend
        const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
        { return doMultiply(lhs, rhs); }
        ...
    };
    

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

    • STL主要由“用以表现容器、迭代器和算法”的template构成,但也覆盖若干工具性的template,其中一个名为advance,用来将某个迭代器移动某个给定距离
    template<typename IterT, typename DistT>  
    void advance(IterT& iter, DistT d); // 将迭代器向前移动d个单位,d<0则向后移动
    
    • STL有五种迭代器分类
      • Input迭代器只能向前移动,一次一步,只读,且只能读取一次,它们模仿指向输入文件的阅读指针,istream_iterator是这一分类的代表
      • Output迭代器类似,但一切只为输出,只能向前移动,一次一步,只能写,且只能涂写一次,它们模仿指向输出文件的涂写指针,ostream_iterator是代表。这两种是威力最小的迭代器,它们只适合一次性操作算法
      • forward迭代器,可以做前述两种分类所能做的每一件事,而且可以读或写其所指物一次以上,因此可施行于多次性操作算法。STL未提供单向linked_list,但某些程序库有,指入这种容器的迭代器就属于forward迭代器。
      • Bedirectional迭代器比上一个威力更大:除了可以向前移动,还可以向后移动。STL的list,set,multiset,map和multimap的迭代器都属于这一类
      • random access迭代器威力最大,可以执行迭代器算术,常量时间内可以向前或向后跳跃任意距离。内置指针也可以当random迭代器用。vector,deque和string提供的迭代器都是这一类
    • 这5种分类,c++标准程序库分别提供专属的tag struct加以确认,这些struct之间的继承关系是有效的is-a关系,所有forward迭代器都是input迭代器,依此类推
    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_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; } //针对random_access迭代器使用迭代器算术运算  
        else {
            if (d >= 0) { while (d--) ++iter; }
            else { while (d++) --iter; }  
        }
    }
    
    • 这种做法必须先判断iter是否为random_access迭代器。需要取得类型的某些信息。那就是traits让你进行的事:它们允许在编译期间取得某些类型信息。traits并不是c++关键字或一个预先定义好的构件,而是一种技术,也是C++程序员共同遵守的协议。这个技术的要求之一是,它对内置类型和用户自定义类型的表现必须一样好
    • iterator_traits 的运作方式是对于每一个 IterT 类型,在struct iterator_traits<IterT>中声明一个名为iterator_category 的 typedef,这个 typedef用来确认IterT的迭代器分类。iterator_traits 通过两部分实现这一点。首先,它强制要求任何用户定义迭代器类型必须嵌套一个名为 iterator_category的typedef,用来确认适合的tag struct。例如,deque的迭代器可随机访问,所以一个deque iterator的class看起来就像这样
    template < ... >
    class deque {
    public:
      class iterator {
      public:
        typedef random_access_iterator_tag iterator_category;
        ...
      };
      ...
    };
    
    • 然而,list的iterator是双向的,所以它们是这样的
    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特别针对指针类型提供一个局部特化版本。由于指针的行径与ramdom access迭代器类似,所以iterator_traits为指针指定的迭代器类型如下
    template<typename IterT>
    struct iterator_traits<TierT*> {
        typedef random_access_iterator_tag iterator_category;
        ...
    };
    
    • 到此为止,你了解了如何设计和实现一个 traits class
      • 识别你想让它可用的关于类型的一些信息,例如,对于iterator来说,就是它们的iterator category
      • 为该信息选择一个名称,如iterator_category
      • 提供一个 template和一组specialization(如iterator_traits),内含你希望支持的类型的信息
    • 给出了iterator_traits(实际上是 std::iterator_traits)就可以改善 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))  
        ...
    }
    
    • 虽然看起来合理,但它不是我们想要的。首先它会导致编译问题,这点到条款48再讨论。现在有一个更根本的问题,IterT的类型在编译期间是已知的,所以iterator_traits<IterT>::iterator_category也可以在编译期间被确定。但if语句运行时才会确定,运行时才做编译期间就能做的事情不仅浪费了时间,还可能造成执行码膨胀。我们想要一个条件式判断“编译期核定成功”之类型,取得这种行为的办法就是重载。为了让advance的行为如我们所期望,我们需要做的是产生两版重载函数,内含advance的本质内容,但各自接受不同类型的iterator_category对象,这里将这两个函数取名为doAdvance
    template <typename IterT, typename DistT>
    void doAdvance(IterT& iter, DistT d, random_access_iterator_tag)
    {
        iter += d;
    }
    
    template <typename IterT, typename DistT>
    void doAdcance(IterT& iter, DistT d, bidirectionl_iterator_tag)
    {
        if(d >= 0) { while (d--) ++iter; }
        else { while (d++) --iter; }
    }
    
    template <typename IterT, typename DistT>
    void doAdvance(IterT& iter, DistT d, input_iterator_tag)
    {
        if (d < 0) {
            throw out_of_range("Negative distance");
        }
        while (d--) ++iter;
    }
    
    • 由于forward_iterator_tag继承自input_iterator_tag,因此针对input_iterator_tag的doAdvance版本也能处理 forward iterator,这就是在不同的iterator_tag struct之间继承的动机。有了上面定义的这些重载的函数,那么advance需要做的只是调用它们并额外传递一个对象
    template<typename IterT, typename DistT>
    void advance(IterT& iter, DistT d)
    {
        doAdvance(
        iter, d,
        typename
        std::iterator_traits<IterT>::iterator_category()
        );                            
    }
    
    • 现在能够概述如何使用一个 traits class 了:
      • 创建一套重载的 "worker" function或者 function template(如doAdvance),它们的差异只在于traits参数。令每个函数实现码与接受的traits信息一致地实现
      • 创建一个 "master" function或者 function templates(如advance)调用上条的"worker" function,传递通过 traits class所提供的信息

    48 认识template元编程

    • 模板元编程(template mataprogramming,TMP)是编写C++程序并执行于编译期的过程,所谓template mataprogram(模板元程序)是以C++写成,执行于C++编译器内的程序,一旦TMP程序结束执行,其输出,也就是从templates具现出来的若干C++源码,便会一如往常地被编译
    • TMP于1990s初期被发现,自从template加入C++,TMP底层特性便被引进了。TMP有两个伟大效力,第一它让某些事情变得更容易,第二template program执行于编译期,工作可以从编译期转移至执行期,某些错误可以被提前侦测,另外使用TMP的程序在每一方面都更高效,较小的可执行文件,较短的运行期,较少的内存,代价是编译时间变长了,再来看看条款47中的advance
    template<typename Iter, typename DistT>
    void advance(IteT& iter,DistT d)
    {
        if(typeid(typename std::iterator_traits<IterT>::iterator_category)
            == typeid(std::random_access_iterator_tag)) {
            iter += d;
        }
        else {
            if(d >= 0){ while(d--) ++iter; }
            else { while(d++) --iter; }
        }
    }
    
    • typeid-based解法效率比traits解法低,因为在此方案中,类型测试发生在运行期而不是编译期,并且运行期类型测试代码会被连接在可执行文件中。TMP比正常的C++程序更高效,traits解法就是TMP,一些东西在TMP比在正常的C++更容易,advance提供一个好例子。advance的typeid-based实现方式可能导致编译期问题
    std::list<int>::iterator iter;
    ...
    advance(iter, 10); // 移动iter向前走10个元素
    // 上述实现无法通过编译
    // 下面这一版的advance便是针对上述调用产生的
    // 将template参数iterT和DistT分别替换为iter和10的类型之后得到
    void advance(std::list<int>::iterator& iter, int d)
    {
        if (typeid(typename std::iterator_traits<std::list<int>::iterator>::iterator_category)
            ==typeid(std::random_access_iterator_tag)) {
            iter += d; // 错误
        }
        else {
            if(d >= 0) { while(d--) ++iter; }
            else { while(d++) --iter; }
        }
    }
    
    • 问题出在+=操作符,list::iterator是bidirectional迭代器,不支持+=,只有random access迭代器才支持+=。测试typeid的那一行总是会因为list<int>::iterator而失败,所以不会执行+=那一行,但编译器必须确保所有源码都有效,即使是不会执行的代码。而traits-based TMP解法针对不同类型执行不同代码,被拆分为不同函数,不会出现上述问题
    • TMP已被证明是个图灵完全机器,也就是说它的威力足以计算任何事物。可以使用TMP声明变量、执行循环、编写调用函数......为了再次简单认识事物在TMP中如何运作,来看看循环,TMP没有真正的循环构件,循环由递归完成。TMP的递归不涉及递归函数调用,而是涉及递归模板实例化。下面是一个TMP例子,编译期计算阶乘,示范如何通过递归模板具体化实现循环,以及如何在TMP中创建和使用变量
    template<unsigned n>
    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<n>内部指涉另一个template实例Factorial<n-1>之时,template特化版本Factorial<0>是递归的结束。每个Factorial template实例都是一个struct,每个struct都使用enum hack声明一个名为value的TMP变量,用来保存当前计算所得的阶乘值。TMP以递归模板实例化取代循环,每个实例有自己的一份value,每个value有其循环内适当值
    // 可以这样使用Factorial
    int main()
    {
        std::cout << Factorial<5>::value; // 印出120
        std::cout << Factorial<10>::value; // 印出3628800
    }
    
    • 用Factorial示范TMP就像用hello world示范编程语言一样。为了领悟TMP之所以值得学习,就要先对它能够达成什么目标有一个比较好的理解,举三个例子
      • 确保量度单位正确。使用TMP可以确保在编译期所有量度单位的组合都正确,不论其计算多么复杂
      • 优化矩阵运算
      • 可以生成客户定制之设计模式实现品。使用policy-based design之TMP-based技术,有可能产生一些template用来表述独立的设计选项,然后可以任意结合它们,导致模式实现品带着客户定制的行为

    相关文章

      网友评论

        本文标题:【Effective C++(7)】模板与泛型编程

        本文链接:https://www.haomeiwen.com/subject/sysbnxtx.html