• Effective C++ —— 模板与泛型编程(七)


      C++ templates的最初发展动机很直接:让我们得以建立“类型安全”的容器如vector,list和map。然而当愈多人用上templates,他们发现templates有能力完成愈多可能的变化。容器当然很好,但泛型编程——写出的代码和其所处理的对象类型彼此独立——更好。STL算法如for_each, find 和 merge 就是这一类编程的成果。最终人们发现,C++ template机制自身是一部完整的图灵机:它可以被用来计算任何可计算的值。于是导出了模板元编程,创造出“在C++编译器内执行并于编译完成时停止执行”的程序。容器反倒只成为C++ template 上的一小部分。然而,尽管template 的应用如此宽广,有一组核心观念一直支撑着所有基于template的编程。那些观念便是本章焦点。

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

      面向对象编程世界总是以显式接口和运行期动态解决问题。如下代码所示:

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

      (1)由于w的类型被声明为Widget,所以w必须支持Widget接口。我们可以在源码中找出这个接口,看看它是什么样子,所以我们称为一个显式接口,也就是它在源码中明确可见。

      (2)由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态,也就是说将于运行期根据w的动态类型(条款37)决定究竟调用哪一个函数。

      Templates及泛型编程的世界,与面向对象有根本不同。在此世界中显式接口和运行期多态仍然存在,但重要性降低。反倒是隐式接口和编译器多态更显重要了。如下:

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

      (1)w 必须支持哪一种接口,系由template中执行于w身上的操作来决定。本例看来w的类型T好像必须支持size,normalize和swap成员函数,copying函数(用来建立temp),不等比较(用来比较someNasty-Widget)等等。这一组表达式(对此template而言必须有效编译)便是T必须支持的一组隐式接口

      (2)凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化,使这些调用得以成功。这样的具现行为发生在编译期。“以不同的template参数具现化function templates(函数模板)”会导致调用不同的函数,这个便是所谓的编译期多态。(“哪一个函数应该被调用”——发生在编译期; “哪一个virtual函数该被绑定”——发生在运行期)。

      通常,显式接口由函数签名式(也就是函数名称、参数类型、返回类型)构成。

      隐式接口就完全不同了。它并不基于函数签名式,而是由有效表达式组成。

    故而
      1. classes 和 templates 都支持接口和多态。

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

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

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

       下面代码,从C++ 的角度来看,声明template参数时,class 和 typename 的意义完全相同:

    template<class T> class Widget;
    template<typename T> class Widget;

      然而,C++ 并不总是把class 和 typename 视为等价。有时候你一定得使用typename。考虑下面代码:

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

      代码中有两个local变量。iter的类型是C::const_iterator,实际是什么必须取决于template参数C。(1)template内出现的名称如果相依于某个template参数,称之为从属名称。(2)如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称。C::const_iterator就是这样一个名称。实际上它还是个(3)嵌套从属类型名称,也就是个嵌套从属名称并且指涉某类型。int是一个并不倚赖任何template参数的名称。这样的名称是所谓非从属名称。

      上面代码的问题在于,我们认为的“C::const_iterator”是个类型,但当编译器开始解析tempalte print2nd时,尚未确知C是什么东西。C++有个规则:如果解析器在template中遭遇个嵌套从属名称,它便假设这名称不是个类型,除非你告诉它是。所以缺省情况下嵌套从属名称不是类型。这也就是上面代码中“无效代码”的缘由。如果要正确使用,我们必须告诉C++说C::const_iterator是个类型。只要紧临它之前放置关键字typename即可:

    template<typename C>
    void print2nd(const C& container)
    {
        if (container.size() >= 2)
        {
            typename C::const_iterator iter(container.begin());    // 无效代码
            ...
        }
    }

      规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename。typename 只被用来验明嵌套从属类型名称;其他名称不该有它的存在。如下:

    template<typename C>            //允许使用typename
    void func(const C& container,   // 不允许使用typename
                    typename C::iterator iter);   // 允许使用typename

      注意:“typename必须作为嵌套从属类型名称的前缀词”这一规则的例外是,typename 不可以出现在base classes list (基类列,类继承列表)内的嵌套从属类型名称之前,也不可在member initialization list(成员初值列)中作为base class修饰符。如下:

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

      有时,嵌套从属类型名称过长,我们可以考虑建立一个typedef。如下:

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

    故而:

      1. 声明template参数时,前缀关键字class 和 typename 可互换。

      2. 请使用关键字 typename 标识嵌套从属类型名称;但不得在base classes list (基类列)或 member initialization list(成员初值列)内以它作为base class 修饰符。

    条款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);
            ...
    };
    
    ...
    
    class MsgInfo {  ... };
    
    template<typename Company>
    class MsgSender
    {
        public:
            ...
            void sendClear(const MsgInfo& info)
            {
                std::string msg;
                // here ,根据info产生信息
                Company c;
                c.sendCleartext(msg);
            }
            void sendSecret(const MsgInfo& info)
            { ... }   // 调用sendEncrypted
    };
    
    // 假设,现在我们想在每次发生信息时志记(log)
    template<typename Company>
    class LoggingMsgSender : public MsgSender<Company>
    {
        public:
            ...
            // 这里sendClearMsg名称不同于其父类的sendClear
            // 这是一个好设计,因为它避免遮掩“继承而得的名称”,条款33
            // 也避免重新定义一个继承而得的non-virtual函数,条款36
            void sendClearMsg(const MsgInfo& info)
            {
                // 将“传送前”的信息写至log
                sendClear(info);
                // 将“传送后”的信息写至log
            }
            ...
    };

      上面代码的问题在于,当编译器遭遇class template LoggingMsgSender定义式时,并不知道它继承什么样的class。当然它继承的是MsgSender<Company>,但其中Company是个template参数,不到后来(当LoggingMsgSender被具现化)无法确切知道它是什么。而如果不知道Company是什么,就无法知道class MsgSender<Company>看起来像什么——更明确地说是没办法知道它是否有个sendClear函数

      为了使问题更具体化,考虑下面情况,模板特化参见模版的特化与偏特化

    // CompanyZ 坚持只能使用加密通讯
    class CompanyZ
    {
        public:
            ...
            // 不提供sendCleartext函数
            void sendEncrypted(const std::string& msg);
            ...
    };
    
    // 一般性的MsgSender template 对 CompanyZ并不合适
    // 因为MsgSender提供了一个sendClear函数,而这对于CompanyZ并不合理
    // 可以针对CompanyZ产生一个MsgSender特化版:
    template<>      // 一个全特化
    class MsgSender <CompanyZ>
    {
        public:
            ...
            void sendSecret(const MsgInfo& info)
            { ... }   // 调用sendEncrypted
            ...
    };

      此时,对于LoggingMsgSender,当base class 被指定为MsgSender <CompanyZ>时,调用sendClearMsg仍不合法,因为CompanyZ并未提供sendClear函数!这就是为什么C++拒绝这个调用的原因:它知道base class templates有可能被特化,而那个特化版本可能不提供和一般性template相同的接口。因此它往往拒绝在templatized base classes(模板化基类)内寻找继承而来的名称。就某种意义而言,当我们从Object Oriented C++ 跨进 Template C++(条款01),继承就不像以前那般畅行无阻了。

      有三个办法令C++“不进入templatized base classes观察”的行为失效

      (1)在base class 函数调用动作之前加上“this->":

    template<typename Company>
    class LoggingMsgSender : public MsgSender<Company>
    {
        public:
            ...
            void sendClearMsg(const MsgInfo& info)
            {
                // 将“传送前”的信息写至log
                this->sendClear(info);     //成立,假设sendClear将被继承
                // 将“传送后”的信息写至log
            }
            ...
    };

      (2)使用using声明式,条款33.

    template<typename Company>
    class LoggingMsgSender : public MsgSender<Company>
    {
        public:
            using MsgSender<Company>::sendClear;  // 告诉编译器,请假设sendClear位于base class内
            ...
            void sendClearMsg(const MsgInfo& info)
            {
                // 将“传送前”的信息写至log
                sendClear(info);   
                // 将“传送后”的信息写至log
            }
            ...
    };

      (3)明白指出被调用的函数位于base class 内

    template<typename Company>
    class LoggingMsgSender : public MsgSender<Company>
    {
        public:
            ...
            void sendClearMsg(const MsgInfo& info)
            {
                // 将“传送前”的信息写至log
                // OK, 假设sendClear将被继承下来
                // 但有一个问题,如果被调用的是virtual函数,
                // 下面语句的明确资格修饰会关闭“virtual绑定行为”
                MsgSender<Company>::sendClear(info);  
                // 将“传送后”的信息写至log
            }
            ...
    };

    故而:

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

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

      本条款无非采用“共性与变性分析”(找出共同部分和变化部分,分别分析处理)。考虑如下代码:

    template<typename T,     // 类型参数
                    std::size_t n>        // 非类型参数
    class SquareMatrix
    {
        public:
            ....
            void invert();   // 支持矩阵逆置
    };
    
    // 如下调用会具现化两份invert
    SquareMatrix<double, 5> sm1;
    ...
    sm1.invert();  // 调用SquareMatrix<double, 5>::invert
    SquareMatrix<double, 10> sm2;
    ...
    sm2.invert();    // 调用SquareMatrix<double, 10>::invert

      (1)下面对SquareMatrix作第一次修改:

    // SquareMatrixBase只对“矩阵元素对象的类型”参数化
    // 因此对于某给定之元素对象类型,所有矩阵共享同一个(也是
    // 唯一一个)SquareMatrixBase class。它们也将因此共享
    // 这唯一一个class内的invert
    template<typename T>        // 只对“矩阵元素对象的类型”参数化
    class SquareMatrixBase
    {
        protected:
            ....
            void invert(std::size_t matrixSize); 
    };
    
    template<typename T, std::size_t n>
    class SquareMatrixBase : private SquareMatrixBase<T> // private, 条款39
    {
        private:
            using SquareMatrixBase<T>::invert;     // 避免遮掩base版的invert,条款33
        public:
            ....
            void invert()
            { this->invert(n); }    // "this->" 记号,条款43
    };

      (2)上面代码仍存在一个问题: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(pMem) { } ... private: std::size_t size; // 矩阵大小 T*pData; // 指针,指向矩阵数值 }; // SquareMatrix对象有着不同的类型 // method1:某些实现版本也许会决定将矩阵数据存储在SquareMatrix对象内部 // 这可能导致对象自身非常大 template<typename T, std::size_t n> class SquareMatrix : private SquareMatrixBase<T> { public: SquareMatrix() : SquareMatrixBase<T>(n, data) { } ... private: T data[n*n]; }; // method2:把每一个矩阵的数据放进heap(通过new来分配内存)
    // SquareMatrix对象有着不同的类型
    template<typename T, std::size_t n> class SquareMatrix : private SquareMatrixBase<T> { public: SquareMatrix() : SquareMatrixBase<T>(n, 0) // 将base class的数据指针设为null pData(new T[n*n]) // 为矩阵内容分配内存,将指向该内存的指针存储起来 { this->setDataPtr(pData.get()); } // 将它的一个副本交给base class ... private: boost::scoped_array<T> pData; // 条款13 };

      经过上述修改,SquareMatrix成员函数可以单纯地以inline方式调用base class版本,(1)后者由“持有同型元素”(不论矩阵大小)之所有矩阵共享。与此同时,不同大小的(2)SquareMatrix对象有着不同的类型,所以即使(例如SquareMatrix<double, 5>和SquareMatrix<double, 10>)对象使用相同的SquareMatrixBase<double>成员函数,我们也没机会传递一个SquareMatrix<double, 5>对象到一个期望获得SquareMatrix<double, 10>的函数去。

      硬是绑定矩阵尺寸(原始版本)的那个invert版本,有可能生成比共享版本更佳的代码;另一方面,不同大小的矩阵只拥有单一版本的invert(修改后的版本),可减少执行文件大小,也就因此降低程序的working set大小,并强化指令高速缓存区内的引用集中化。对象大小有时也是一个需要考虑的效能评比主题。

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

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

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

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

      所谓“智能指针”是“行为像指针”的对象,并提供指针没有的机能。STL容器的迭代器几乎总是智能指针。真实指针做得很好的一件事是,支持隐式转换。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* pct2 = 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的不同具现体之间并不存在什么与生俱来的固有关系。所以编译器视SmartPtr<Middle>和SmartPtr<Top>为完全不同的classes。

      Templates和泛型编程

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

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

      以上代码的意思是,对任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T>——因为SmartPtr<T>有个构造函数接受一个SmartPtr<U>参数。这一类构造函数根据对象u创建对象t,而u和t的类型是同一个template的不同具现体,有时我们称之为泛化copy构造函数上面泛化copy构造函数并未被声明为explicit,因为我们需要此泛化构造函数支持隐式转换(explicit会限制类型的隐式转换)

      然而,此泛化构造函数提供的东西比我们需要的更多。我们需要根据一个SmartPtr<Bottom>创建一个SmartPtr<Top>,却不希望根据一个SmartPtr<Top>创建一个SmartPtr<Bottom>;我们也不希望根据一个SmartPtr<double>创建一个SmartPtr<int>。因此,我们必须从某方面对这一member template所创建的成员函数群进行拣选或筛除。假设SmartPtr遵循auto_ptr 和 tr1::shared_ptr所提供的榜样,也提供一个get成员函数,返回智能指针对象(条款15)所持有的那个原始指针的副本,那么:

    template<typename T>
    class SmartPtr
    {
        public:
            template<typename U>                         // member template
            SmartPtr(const SmartPtr<U>& other)
                : heldPtr(other.get()) { ... }   // 成员初值列 隐式转换
            T* get() const { return heldPtr; }
            ...
        private:
            T* heldPtr;          // 持有内置(原始)指针
    };

      我们使用成员初值列来初始化SmartPtr<T>之内类型为T* 的成员变量,并以类型为U*的指针作为初值。这个行为只有当“存在某个隐式转换可将一个U*指针转为一个T*指针”时才能通过编译

      member function templates(成员函数模板)的效用不限于构造函数,他们常扮演的另一个角色是支持赋值操作。

    template<class T>
    class shared_ptr
    {
        public:
            template<Y>
                explicit shared_ptr(Y* p);  // 构造函数
            template<Y>
                shared_ptr(shared_ptr<Y> const& r);    // copy构造函,没有被声明为explicit,支持隐式转换
            template<Y>
                explicit shared_ptr(weak_ptr<Y> const& r);
            template<Y>
                explicit shared_ptr(auto_ptr<Y> & r);   // 不带const,auto_ptr会改变传入的对象
    
            template<Y>
                shared_ptr& operator=(shared_ptr<Y> const& r);  // copy赋值操作符
            template<Y>
                shared_ptr& operator=(auto_ptr<Y> & r);
            ...
    };

      上述所有构造函数都是explicit,唯有“泛化copy构造函数”除外。那意味从某个shared_ptr类型隐式转换至另一个shared_ptr类型是被允许的(调用到copy构造函数),但从某个内置指针或从其他智能指针类型进行隐式转换则不被认可。另一点,传递给tr1::shared_ptr构造函数和assignment操作符的auto_ptr并未被声明为const,这是因为条款13说过,当你复制一个auto_ptr,它们其实被改动了。

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

    template<class T>
    class shared_ptr
    {
        public:
            shared_ptr(shared_ptr const& r);   // non-template copy构造函数
            template<Y>
            shared_ptr(shared_ptr<Y> const& r);    // copy构造函数
    
            shared_ptr& operator=(shared_ptr const& r); // non-template copy赋值操作符
            template<Y>
            shared_ptr& operator=(shared_ptr<Y> const& r);  // copy赋值操作符
            ...
    };

    故而:
      1. 请使用member function templates(成员函数模板)生成“可接受所有兼容类型”的函数。

      2. 如果你声明member templates用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。(member templates并不改变语言规则)。

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

      条款24讨论过为什么唯有non-member函数才有能力“在所有实参身上实施隐式类型转换”,本条款在此基础上将条款24例子中的Rational和operator*模板化了:

    template<typename T>
    class Rational
    {
        public:
            Rational(const T& numerator = 0,    // 条款20告诉你为什么参数以passed by reference方式传递
                        const T& denominator = 1);
            const T numerator() const;   // 条款28告诉你为什么返回值以passed by value方式传递
            const T numerator() const;  // 条款03告诉你为什么它们是const
            ...
    };
    // 非类成员,亦非友元,而仅仅只是一个function templates template
    <typename T> const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) { .... } // 我们希望一下调用顺利通过编译,毕竟它正是条款24所列同一份代码,不同的仅是成了templates: Rational<int> oneHalf(1, 2); // ok Rational<int> result = oneHalf * 2; //错误,无法通过编译

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

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

      只要利用一个事实,我们就可以缓和编译器在template实参推导方面受到的挑战:template class 内的friend 声明式可以指涉某个特定函数。那意味class Rational<T>可以声明operator*是它的一个friend函数。Class templates并不倚赖template实参推导(后者只施行于function templates身上),所以编译器总是能够在class Rational<T>具现化时得知T。

    template<typename T>
    class Rational
    {
        public:
         // 定义成non-member函数在于支持“在所有实参身上实施隐式类型转换”,operator*函数定义在Rational类里面,是为了此函数被自动具现化,原因如下:
         // 当Rational对象被声明的时候,class Rational<int>也就被具现化出来了,而作为过程的一部分,friend函数operator*也就被自动具现化.
        // 现在可以通过编译,却仍然还不能通过链接,因为只提供了函数声明,而没有提供函数定义,试图使用class外的operator*函数定义是不行的:如果我们自己声明了一个函数,就有责任定义那个函数
    friend
    // 声明operator*函数 const Rational operator*(const Rational& lhs, const Rational& rhs); // 上面语句等价“friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);” };
    template
    <typename T> // 定义operator*函数 const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) { .... }

      现在对operator*的混合式调用可以通过编译了,因为当对象oneHalf被声明为一个Rational<int>,class Rational<int>于是被具现化出来,而作为过程的一部分,friend函数operator*(接受Rational<int>参数)也就被自动声明出来(成为一个特定函数(函数已经是存在状态),而不再是一个函数模板,也即不再需要进行“模板(template)实参推导”,直接进入函数调用时的实参推导,而此时可以使用隐式转换函数)后者身为一个函数而非函数模板,因此编译器可在调用它时使用隐式转换函数(例如Rational 的non-explicit构造函数)。这便是混合式调用之所以成功的原因。但此段代码虽可以通过编译,却无法连接。这里暂时插播一下“Rational内声明operator*的语法”。

      在一个class template内,template 名称可被用来作为“template 和其参数”的简略表示方式,所以在Rational<T>内我们可以只写Rational而不必写Rational<int>。

      现在回头考虑上面无法连接的问题:混合式代码通过了编译,因为编译器知道我们要调用哪个函数,但那个函数只被声明于Rational内,并没有被定义出来。我们意图令此class外部的operator* template 提供定义式,但是行不通——如果我们自己声明了一个函数,就有责任定义那个函数。(friend函数可以在类外进行定义,这里无法连接,是因为此friend函数为template吗?)既然我们没有提供定义式,连接器当然找不到它!

      (1)一个简单的方法就是将operator*函数本体合并至其声明式内

      template<typename T>
    class Rational
    {
        public:
            friend         // 声明operator*函数
                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

      (2)条款30所说,定义于class 内的函数都暗自成为inline,包括像operator*这样的friend函数。你可以将这样的inline声明所带来的冲击最小化,做法是令operator*不做任何事情,只调用一个定义于class外部的辅助函数。“令friend函数调用辅助函数”对于复杂的函数而言是值得的。

    template<typename T> class Rational; // 前向声明Rational template
    template<typename T>
    const Rational<T> doMultiply(const Rational<T>& lhs,
                                                const Rational<T>& rhs);
    
    template<typename T>
    class Rational
    {
        public:
            .....
        friend         // 声明operator*函数
            const Rational operator*(const Rational& lhs,
                                                const Rational& rhs)
              // Rational已具现化 {
    return doMultiply(lhs, rhs); } ... }; // 许多编译器实质上会强迫你把所有template定义式放进头文件内 template<typename T> // 若有必要,在头文件内定义helper template const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs); { return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator()); }

      作为一个template,doMultiply当然不支持混合式乘法,但它其实也不需要。它只被operator*调用,而operator*支持了混合式操作(通过隐式转换函数完成)。

    故而:

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

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

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

    template<typename IterT, typename DistT>
    void advance(IterT& iter, DistT d);     // d < 0 则向后移动

      STL有5类迭代器,C++标准程序库分别提供专属的卷标结构(tag struct)加以确认:

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

      既然我们已经知道STL迭代器有着不同的能力,那么我们会希望运用其优势:

    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必须能够施行于内置类型”意味“类型内的嵌套信息”这种东西出局了,因为我们无法将信息嵌套于原始指针内。因此类型的traits信息必须位于类型自身之外。标准技术是把它放进一个template及其一个或多个特化版本中。这样的templates在标准程序库中有若干个,其中针对迭代器者被命名为iterator_traits:

    template<typename IterT>
    struct iterator_traits;

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

      iterator_traits以两个部分实现上述所言。首先它要求每一个“用户自定义的迭代器类型”必须嵌套一个typedef,名为iterator_category,用来确认适当的卷标结构(tag struct)。如deque的迭代器可随机访问,所以一个针对deque迭代器而设计的class看起来会是这样:

    template< ... >
    class deque
    {
        public:
            class iterator
            {
                public:
                    typedef random_access_iterator_tag iterator_category;
                    ...
            };
            ...
    };
    
    // 至于iterator_traits,只是鹦鹉学舌般的响应iterator class 的嵌套式typedef:
    template<typename IterT>
    struct iterator_traits
    {
        typedef typename IterT::iterator_category iterator_category;
        ....
    };

      这对用户自定义类型行得通,但对指针(也是一种迭代器)行不通,因为指针不可能嵌套typedef(所以不存在“IterT::iterator_category”)。为了支持指针迭代器,iterator_traits特别针对指针类型提供一个偏特化版本。由于指针的行径与random access迭代器类似,所以iterator_traits为指针指定的迭代器类型是:

    template<typename IterT>
    struct iterator_traits<IterT*>  // template 偏特化,针对内置指针
    {
        typedef random_access_iterator_tag iterator_category;
        ....
    };

      现在,我们来小结一下如何设计并实现一个traits class:

      (1)确认若干你希望将来可取得的类型信息。例如对迭代器而言,我们希望将来可取得其分类(category)。

      (2)为该信息选择一个名称(例如 iterator_category)。

      (3)提供一个template 和一组特化版本(针对指针),内含你希望支持的类型相关信息。

      现在,我们有了iterator_traits(实际上是std::iterator_traits,因为它是C++标准程序库的一部分),我们可以对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类型在编译期间获知,所以std::iterator_traits<IterT>::iterator_category也可在编译期间确定。但if语句却是在运行期才会核定。为什么将可在编译期完成的事延到运行期才做呢?这不仅浪费时间,也造成可执行文件膨胀。

      我们真正想要的是一个条件式判断“编译期核定成功”之类型。恰巧C++有一个取得这种行为的办法:重载(利用编译期完成的“重载解析机制”)。。当你重载某个函数f,你必须详细叙述各个重载件的参数类型,而编译器便会在编译期间匹配最适当的那个重载函数。如下:

    template<typename IterT, typename DistT>
    void doAdvance(IterT& iter, DistT d   // random access 迭代器
                            std::random_access_iterator_tag)
    {
        iter += d;
    }
    
    template<typename IterT, typename DistT>
    void doAdvance(IterT& iter, DistT d   
                            std::bidirectional_access_iterator_tag)
    {
        .....
    }
    
    // forward_iterator_tag public 继承自input_iterator_tag
    template<typename IterT, typename DistT>
    void doAdvance(IterT& iter, DistT d  
                            std::input_iterator_tag)
    {
        ....
    }
    
    // 有了这些doAdvance重载版本,advance需要做的只是调用它们并额外
    // 传递一个对象,后者必须带有适当的迭代器分类。
    // 于是编译器运用“重载解析机制”(发生于编译期间)调用适当的实现代码
    template<typename IterT, typename DistT>
    void advance(IterT& iter, DistT d)
    {
        doAdvance(
            iter, d,         //
            typename std::iterator_traits<IterT>::iterator_category()
        );
    }

      现在,我们小结如何使用一个traits class:

      (1)建立一组重载函数或函数模板(doAdvance),彼此间的差异只在于各自的traits参数。令每个函数实现码与其接受之traits信息相应和。

      (2)建立一个控制函数或函数模板(advance),它调用上述那些doAdvance函数并传递traits class 所提供的信息。

      Traits 广泛用于标准程序库。其中当然有上述讨论的iterator_traits,除了供应iterator_category还供应另四份迭代器相关信息(其中最有用的是value_type,条款42)。此外还有char_traits用来保持字符类型的相关信息,以及numericlimits用来保存数值类型的相关信息,例如某数值类型可表现之最小值和最大值等等;命名为numeric_limits没有遵守traits class 常以“traits”结束的风格。

      TR1(条款54)导入许多新的traits classes用以提供类型信息,包括is_fundamental<T>(判断T是否为内置类型),is_array<T>(判断T是否为数组类型),以及is_base_of<T1, T2>(T1和T2相同,抑或T1是T2的base class)。总计TR1一共为标准C++添加了50个以上的traits classes。

    故而:

      1. Traits classes使得“类型相关信息”在编译期可用。它们以templates(针对用户自定义类型)和“templates特化”(针对指针)完成实现

      2. 整合重载技术后,traits classes 有可能在编译期对类型执行if....else测试。(利用编译期完成的“重载解析机制”)。

    条款48 : 认识template元编程

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

      C++并非是为Template metaprogramming而设计,但自从TMP于1990s初期被发现以后,由于日渐被证明十分有用,其延伸部分很可能加入语言和标准程序库内,使TMP更容易进行。

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

      条款47中有如下伪码:

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

      我们可以使用typeid让其中的伪码成真,取得C++对此问题的一个“正常”解决方案——所有工作都在运行期进行:

    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))
        ...
    }

      条款47指出,这个typeid-based解法的效率比traits解法低,因为在此方案中,(1)类型测试发生于运行期而非编译期,(2)“运行期类型测试”代码会出现在(或说被连接于)可执行文件中。实际上这个例子正可彰显TMP如何能够比“正常的”C++程序更高效,因为traits解法就是TMP。别忘了,traits引发“编译期发生于类型身上的if...else计算”。同时,条款47曾提过advance的typeid-based实现方式可能导致编译期问题,下面就是个例子:

    std::list<int>::iterator iter;
    ...
    advance(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; }
        }
    }

      问题出在我所强调的那一行代码使用了 += 操作符,那便是尝试在一个std::list<int>::iterator身上使用 += ,但是std::list<int>::iterator是个bidirectional迭代器,并不支持+=。只有random access 迭代器才支持 += 。此刻我们知道绝不会执行起 += 那一行,因为测试typeid的那一行总是会因为std::list<int>::iterators而失败,但编译器必须确保所有源码都有效,纵使是不会执行起来的代码!而当iter不是random access 迭代器时“iter += d”无效。与此对比的是traits-based TMP解法,其针对不同类型而进行的代码,被拆分为不同的函数,每个函数所使用的操作(操作符)都可施行于该函数所对付的类型。

      TMP已被证明是个“图灵完全”机器,意思是它的威力大到足以计算任何事物。使用TMP你可以声明变量、执行循环、编写及调用函数……但这般构件相对于“正常的”C++对应物看起来很是不同,例如条款47所展示的TMP if...else条件句是藉由templates和其特化体表现出来。不过那毕竟是汇编语言层级的TMP。针对TMP而设计的程序库(例如Boost’s MPL,条款55)提供更高层级的语法。

      为了再次浮光掠影地认识一下“事物在TMP中如何运作”,让我们看看循环。TMP并没有真正的循环构件,所以循环效果系藉由递归完成。TMP主要是个“函数式语言”,而递归之于这类语言就像电视之于美国通俗文化一样地无法分割。TMP的递归甚至不是正常种类,因为TMP循环并不涉及递归函数调用,而是涉及“递归模板具现化”。TMP的起手程序是在编译期计算阶乘:

    template<unsigned n>
    struct Factorial
    {
        enum { value = n * Factorial<n-1>::value };
    };
    template<>  // 递归终结,模板特化
    struct Factorial<0>
    {
        enum { value = 1 };
    };
    
    // 你便可以这样调用
    std::cout << Factorial<5>::value;  // 打印120

      为求领悟TMP之所以值得学习,很重要一点是先对它能够达成什么目标有一个比较好的理解:

      (1)确保量度单位正确。在科学和工程应用程序中,确保量度单位(例如质量、距离、时间……)正确结合是绝对必要的。如果使用TMP,就可以确保(在编译期)程序中所有量度单位的组合都正确,不论其计算多么复杂。

      (2)优化矩阵运算。如果使用高级、于TMP相关的template技术,即所谓expression templates,就有可能消除那些临时对象并合并循环,这一切都无需改变客户端的用法。

      (3)可以生产客户端定制之设计模式实现品。设计模式如Strategy(条款35),Observer,Visitor等等都可以多种方式实现出来。运用所谓policy-based design之TMP-based技术,有可能产生一些templatles用来表述独立的设计选项,然后可以任意结合它们,导致模式实现品带着客户定制的行为。这项技术已被用来让若干templates实现出智能指针的行为政策,用以在编译期间生成数以百计不同的智能指针类型。这项技术已经超越编程工艺领域如设计模式和智能指针,更广义地成为generative programming(殖生式编程)的一个基础。

    故而:

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

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

  • 相关阅读:
    ios开发遇到的问题
    getopt()——命令行参数分析
    浅谈Samsung Exynos4412处理器
    TTL电平,CMOS电平,232/485电平,OC门,OD门基础知识
    (转载)跟Classic ARM 处理器说拜拜——Atmel SAMA5D3 Xplained开发板评测
    (转载)Quartus II中FPGA的管脚分配保存方法(Quartus II)
    DE2资源集锦
    收到DE2+LCM+ D5M套件,拾回DE2,努力,奋进!
    windows 服务器时间同步失败处理方法
    机械加工仿真软件-----三维弯管机仿真系统
  • 原文地址:https://www.cnblogs.com/yyxt/p/4821359.html
Copyright © 2020-2023  润新知