zoukankan      html  css  js  c++  java
  • Effective C++读书笔记~7 模板与泛型编程

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

    Understand implicit interfaces and compile-time polymorphism.

    显式接口和运行期多态

    面向对象编程中,以显式接口(explicit interface)和运行期多态(runtime polymorphism)解决问题。例如:

    class Widget {
    public:
        /* 显式接口 由函数签名式构成 */
        Widget();
        virtual ~Widget();
        virtual std::size_t size() const;
        virtual void normalize();
        void swap(Widget& other);
        ...
    };
    
    void doProcessing(Widget& w)
    {
        if (w.size() > 10 && w != someNastyWidget) { // size是virtual函数, 运行期根据w动态类型决定
            Widget temp(w);
            temp.normalize(); // normalize是virtual函数, 运行期根据w的动态类型决定
            temp.swap(w);
        }
    }
    

    对于doProcessing参数w,所谓显式接口和运行期多态,具体是指:

    • 由于w类型被声明为Widget,所以w必须支持Widget接口:Widget类对应头文件(如Widget.h)中声明的接口,称为显式接口(explicit interface);
    • 由于Widget的某些成员函数是virtual,w对那些函数的调用将表现出运行期多态(runtime polymorphism),也就是说将于运行期根据w的动态类型(条款37)决定究竟调用哪个函数;

    显式接口通常由函数的签名式(函数名称、参数类型、返回类型)构成,如Widget class的public接口:构造函数、析构函数、函数size,normalize,swap及其参数类型,返回类型,常量性(constness),也包括编译器合成的copy构造函数、copy assignment操作符等。

    隐式接口和编译期多态

    与面向对象不同,在template和泛型编程中,隐式接口(implicit interface)和编译期多态(compile-time polymorphism)更重要。
    隐式接口不基于函数签名式,而是由有效表达式组成。隐式接口在编译期完成检查。具体看下面的例子,

    将doProcessing改写成template版本

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

    T(w的类型)的隐式接口看起来好像有这些约束:

    • 它必须提供一个名为size的成员函数,该函数返回一个整数值;
    • 它必须支持一个operator!= 重载运算符的函数,用于比较2个T类型对象。

    然而,由于操作符重载(operator overloading)的存在,这2个约束都不必满足。一种可能性是,T可以支持size成员函数,而size也可能从base class继承(不必是T自身的)而得到的。size也不必返回一个整数值,也可以不必是一个数值类型,唯一需要的是返回X类型的对象,X对象+int(10的类型)必须能够调用一个operator>。也就是说,X不必支持operator>,也可以是Y类型,而只需要存在一个隐式转换能将X对象转换为Y对象,而Y对象支持operator>。
    类似地,T不需要支持operator!=(调用形式X.operator!=(Y)),也可以是operator!=(X,Y)(重载operator!=),其中T可以转换为X类型,someNastyWidget可以转换为Y类型。
    另外,opeartor&&也可能被重载,而不再是逻辑与运算符。

    函数size, operator>, opeartor&&, opeartor!= 身上的约束条件,很难明确描述,但是整体确认表达式约束条件却很容易。如if语句(if (w.size() > 10 && w != someNastyWidget))的条件必须是bool表达式。

    小结

    1)class和template都支持接口(interface)和多态(polymorphism)。
    2)对class而言,接口是显式的(explicit),以函数签名为中心。多态则是通过virtual函数发生于运行期(通过指针要调用的函数)。
    3)对template参数而言,接口是隐式的(implicit),奠基于有效表达式。多态则是通过template具现化和函数重载解析(function overloading resolution)发生于编译期。

    [======]

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

    Understand the two meaning of typename.

    在声明template参数时,class和typename完全相同。

    // 下面2行等价
    template<class T> class Widget;
    template<tyepname T> class Widget;
    

    然而,class和typename并不总是等价。比如,

    // 错误示例:嵌套从属名称,会被假设为非类型,除非使用typename指定
    template<typename C>
    void print2nd(const C& container)
    {
           if (container.size() >= 2) {
                  C::const_iterator iter(container.begin()); // 不会通过编译. C::const_iterator是嵌套从属名称, 编译器并不会认为这是一个类型
                  ++iter;
                  int value = *iter;
                  cout << value;
           }
    }
    

    嵌套从属名称:template内出现的名称如果依赖于某个template参数,称为从属名词(dependent name)。如果从属名词在class内呈现嵌套状,比如C::const_iterator,称为嵌套从属名词(nested dependent name)。
    看起来,C::const_iterator是一个类型,但实际上编译器会默认认为它是一个成员变量。如果要让编译器认为这是一个类型,就要用typename明确指出。

    if (container.size() >= 2) {
           typename C::const_iterator iter(container.begin()); // OKtypename 指明嵌套从属名C::const_iterator是个类型
        ...
    }
    

    指定嵌套从属类型名称的一般性规则:当你想要在template中指涉一个嵌套从属类型名称时,在紧邻它的前一个位置上放关键字typename即可。
    例外情况:typename不可以出现在base class list(基类继承列表)内嵌套从属类型名称前,也不可以在构造函数的member initialization list (成员初值列表)中作为base class修复。例如,

    template<typename T>
    class Derived: public Base<T>::Nested { // 基类继承列表,不允许出现typename
    public:
        explicit Derived(int x) : Base<T>::Nested(x) { // 构造含的member initialization list,不允许出现typename
            typename Base<T>::Nested temp; // 嵌套从属类型名称,需要前缀typename
            ...
        }
    };
    
    typename另外一个用途:用typedef typename 为嵌套从属类型名定义一个简短的别名。
    
    template<typename IterT>
    void workWithIterator(IterT iter)
    {
        // 类型为IterT之对象所指之物的类型
        typedef typename std::iterator_traits<IterT>::value_type value_type; // 定义别名
        value_type temp(*iter);
        ...
    }
    

    小结

    1)声明template参数时,前缀关键字class和typename可互换;
    2)请使用关键字typename标识嵌套从属类型名称;但不得在base class list(基类列)或member initialization list(成员初值列)内以它作为base class修饰符;
    3)typename相关规则在不同编译器有不同的实践;

    [======]

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

    Know how to access names in templatized base classes.

    我们定义如下基于template的class,用于传递消息

    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;
                  根据info参数信息;
                  Company c;
                  c.sendCleartext(msg); // OK
           }
           void sendSecret(const MsgInfo& info) {
           }
    };
    
    // 带日志功能的消息发送类
    template<typename Company>
    class LogginMsgSender : public MsgSender<Company> {
    public:
           void sendClearMsg(const MsgInfo& info) { // 信息传递函数, 不与base MsgSender<Company>的同名, 避免遮掩继承而得到的non-virtual函数
                  将“传送前”的信息写至log;
                  sendClear(info); // 报错:编译器无法识别该函数
                  将“传送后”的信息写至log;
           }
    };
    

    编译器遇到派生类template LogginMsgSender的定义式时,并不知道它继承什么样的class,因为其中的Company是个template参数,不到LogginMsgSender被具现化,无法确切知道它是什么。如果无法知道class MsgSender是什么,就没办法知道它是否有个sendClear函数。

    对于特例化模板也是一样

    class CompanyZ {
    public:
           void sendEncrypted(const std::string& msg);
    };
    
    // MsgSender针对CompanyZ进行的全特化
    template<>
    class MsgSender<CompanyZ> {
    public:
           void sendSecret(const MsgInfo& info) {
           }
    };
    
    template<typename Company>
    class LogginMsgSender : public MsgSender<Company> {
    public:
           void sendClearMsg(const MsgInfo& info) {
                  将“传送前”的信息写至log;
                  sendClear(info); // 报错:编译器无法识别该函数
                  将“传送后”的信息写至log;
           }
    };
    

    编译器无法识别模板基类内名称解决办法

    1)在模板基类函数调用动作前加上"this->"

    template<typename Company>
    class LogginMsgSender : public MsgSender<Company> {
    public:
           void sendClearMsg(const MsgInfo& info) {
                  将“传送前”的信息写至log;
                  this->sendClear(info); // OK
                  将“传送后”的信息写至log;
           }
    };
    

    2)使用using声明式
    见条款33,using声明式可以将”被遮掩的base class名称“带入一个derived class作用域内。不过这里并不是base class名称被derived class名称遮掩,而是编译器不进入base class作用域内查找,因为编译器不知道MsgSender是什么东西。因此,我们通过using告诉它,让它到base class MsgSender中去寻找。

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

    3)明确调用base class内的函数

    template<typename Company>
    class LogginMsgSender : public MsgSender<Company> {
    public:
           void sendClearMsg(const MsgInfo& info) {
                  将“传送前”的信息写至log;
                  MsgSender<Company>::sendClear(info); // OK
                  将“传送后”的信息写至log;
           }
    };
    

    缺点:如果被调用的是virtual函数,上面的明确调用方式会关闭“virtual绑定行为”。建议使用1)或者2)。

    小结

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

    [======]

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

    Factor parameter-independent code out of templates.

    template的存在是为了节省时间和避免重复代码。

    举个例子,现在你想为固定尺寸的正方形矩阵编写一个template,该矩阵支持逆矩阵运算(matrix inversion)。

    // template导致代码膨胀的一个典型例子
    template<typename T, std::size_t n> // template 支持nxn矩阵, 元素是类型为T的object
    class SquareMatrix {
    public:
        void invert(); // 求矩阵的逆
    };
    
    // 客户端
    SquareMatrix<double, 5> sm1; 
    sm1.invert();                     // 调用SquareMatrix<double, 5>::invert
    SquareMatrix<double, 10> sm2;
    sm2.invert();                     // 调用SquareMatrix<double, 10>::invert
    

    该template会根据客户端调用情况,具现化2份invert,但除了矩阵尺寸不一样,2个函数其余部分完全相同。

    改善:增加一个base class辅助求矩阵逆,将矩阵尺寸从模板参数移除,放到base class的函数参数中 -- 利用函数参数消除非类型模板参数

    // 所有给定元素对象类型的矩阵, 共享同一个SquareMatrixBase
    template<typename T> // 与尺寸无关的base class, 用于方阵
    class SquareMatrixBase { 
    protected: // 为何用protected, 不用public/private? 因为SquareMatrixBase::invert只是"避免derived class代码重复"的一种方法
        void invert(std::size_t matrixSize); // 以给定尺寸求逆矩阵
    };
    
    tempalte<typename T, std::size_t n>
    class SquareMatrix: private SquareMatrixBase<T> { // 为什么是private继承? 不是is-a关系, 而是has-a关系, 辅助求矩阵逆
    private:
        using SquareMatrixBase<T>::invert;    // 避免遮掩base的invert
    public:
        void invert() { this->invert(n); }
    };
    

    带参数的invert移到base class SquareMatrixBase中,这样就只对base class矩阵元素对象的类型 参数化,而不对矩阵尺寸参数化。因此,对于某给定元素对象类型,所有矩阵共享同一个SquareMatrixBase class。可以有效减少代码量。

    为何函数SquareMatrixBase::invert是protected,而不是public或private?
    因为该函数只是“避免derived class代码重复”的一种方法,客户无需知道,而又需要被derived class调用,因此应该为protected。

    为何使用SquareMatrix类中使用using SquareMatrixBase::invert ?
    根据条款43的方法2,使用using声明式,是告诉编译器请它假设invert位于base class内。

    为何SquareMatrix::invert函数还要使用“this->”记号?
    因为derived class也实现了同名函数invert,如果不这样做,根据条款43,模板化基类内的函数名会被derived class遮掩。

    为何SquareMatrix是private继承SquareMatrixBase,而不是public继承?
    因为SquareMatrix不是SquareMatrixBase,两者并非is-a关系,SquareMatrixBase的存在只是为了帮助SquareMatrix进行求逆矩阵。

    接下来的问题是,
    如何存储矩阵数据?应该存放到 SquareMatrix,还是SquareMatrixBase中?
    矩阵可以用一个一维数组T data[n*n]来存放。而具体要存放多少数据,最清楚的应该是SquareMatrix,而SquareMatrixBase要想操作矩阵,可以用一个指针指向该数组。

    // 使用静态一维数组存放矩阵内容示例
    
    template<typename T>
    class SquareMatrixBase {
    protected:
        SquareMatrixBase(std::size_t n, T* pMem) : size(n), pData(pMem) {}
        void setDataPtr(T* ptr) { pData = ptr; }
        ...
    private:
        std::size_t size; // 矩阵大小
        T* pData; // 指向矩阵内容
    };
    
    template<typename T, std::size_t n>
    class SquareMatrix: private SquareMatrixBase<T> {
    public:
        SquareMatrix() : SquareMatrixBase<T>(n, data) { } // 用构造函数初始化列表将矩阵大小和数据指针传递给base class
        ...
    private:
        T data[n * n]; // 存放矩阵内容
    };
    

    也可以通过动态分配内存,把矩阵的数据放进heap,然后将其交给base class。

    上面SquareMatrix的做法是通过函数参数,消除template非类型参数(矩阵尺寸)。
    有些可能是由于类型参数带来的,如一些平台int和long有相同二进制表述,所以vector和vector的成员函数可能完全相同。有些链接器(linker)会合并完全相同的函数实现码,有些则不会。这种情况下,可以对每个成员函数使用唯一一份底层实现,实现某些成员函数操作强类型指针(strongly typed pointers,即 T),令它们调用另一个操作无类型指针(untyped pointer,即void)的函数,由后者完成实际工作。

    小结

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

    [======]

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

    Use member function template to accept "all compatible types."

    真实指针隐式转换

    智能指针(smart pointer)是“行为像指针”的对象,并提供指针没有的功能。
    如,条款13提到的shared_ptr, unqiue_ptr,如何被利用起来在正确时机自动删除heap-based资源。

    真实指针做得很好的一件事,就是支持隐式转换(implicit conversion),体现在2方面:
    1)derived class指针可以隐式转换为base class指针;
    2)指向non-const对象的指针,可以转换为指向const对象,而无需显式转型(cast);
    例如,

    class Top { ... };
    class Middle: public Top { ... };
    class Bottom: public Middle { ... };
    Top* pt1 = new Middle; // 将Middle*转换为Top* (Middle* => Top*)
    Top* pt2 = new Bottom; // Bottom* => Top*
    const Top* pt2 = pt1;  // const Top* => Top*
    

    智能指针隐式转换

    但如果想要用智能指针模拟上述转换,如何进行?
    比如,要施行以下转换:

    template<typename T>
    class SmartPtr {
    public:
        explicit SmartPtr(T* realPtr);
        ...
    };
    
    // 客户端想要进行的智能指针转换
    SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle); // SmartPtr<Middle> => SmartPtr<Top>
    SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom); // SmartPtr<Bottom> => SmartPtr<Top>
    SmartPtr<const Top> pt3 = pt1; // SmartPtr<Top> => SmartPtr<const Top>
    

    template的2个具现SmartPtr和SmartPtr并没有任何关系,如果希望获得SmartPtr class之间的转换能力,就必须将它们明确编写出来。

    Template和泛型编程(Generic Programming)

    上面例子中,客户端进行智能指针转换时,其实都是创建了一个新的智能指针对象,我们可以关注如何编写智能指针的构造函数,以满足我们的转型需要。

    member template(成员函数模板)

    那么如何为template class编写构造函数呢?
    如果我们为SmartPtr,SmartPtr编写了构造函数,可能在某天,我们又增添了另外一个模板类继承自SmartPtr或者SmartPtr,可能会需要再添加一个构造函数,甚至可能修改原来base class的构造函数。
    实际上,需要构造函数的数量是没有止尽的,因为一个template可以被无线具现化,以至于生成无限量函数。因此,我们需要的不是为SmartPtr写构造函数,而是为它写一个构造模板。这样的模板就是所谓的member function template(简称member template),作用是为class生成函数:

    // 构造模板, 对任何类型的T, U, 可以根据SmartPtr<U>对象生成一个SmartPtr<T>对象
    template<tyepname T>
    class SmartPtr {
    public:
        template<typename U>
        SmartPtr(const SmartPtr<U>& other); // member template,为了生成copy构造函数
        ...
    };
    
    // 客户端调用: SmartPtr<U> => SmartPtr<T>, 其中T和U是任意类型
    SmartPtr<U> pu = SmartPtr<U>(new XXX);
    SmartPtr<T> pt = pu;
    

    这种 用类型为T的模板类对象构造类型为U的模板类的构造函数,我们称之为“泛化copy构造函数”

    为什么上面的泛化copy构造函数并未声明为explicit?
    这是蓄意的,因为原始指针类型之间的转换是隐式转换,无需明白写出转型动作(cast),所以让智能指针效仿这种行径也是合理的。在模板化构造函数(templatized constructor)中略去explicit就是为了这个目的。

    如何编写“返回copy构造函数”?

    完成声明后,如何编写“返回copy构造函数”? 我们希望根据一个SmartPtr创建一个SmartPtr,却不希望根据一个SmartPtr创建一个SmartPtr,因为两者对继承而言是矛盾的(条款32)。同时,我们也不希望根据一个SmartPtr创建一个SmartPtr,因为现实中没有将“int* 转换为double*”的对应隐式转换行为。也就是说,我们必须对member template创建的成员函数进行拣选或者筛除。

    假设SmartPtr遵循unique_ptr和shared_ptr所提供的榜样,也提供get成员函数,返回智能指针对象(条款15)所持有的原始指针的副本,那么我们可以在“构造模板”实现代码中约束转换行为,使得它符合我们的期望:

    template<typename T>
    class SmartPtr {
    public:
        // 构造模板, i.e. 泛化copy构造函数
        template<typename U>
        SmartPtr(const SmartPtr<U>& other) : heldPtr(other.get()) { ... } // 以other的heldPtr初始化this->heldPtr
        
        T* get() const { return heldPtr; } // 返回原始指针的副本
        ...
    private:
        T* heldPtr; // SmartPtr持有的内置指针(原始指针)
    };
    

    我们的构造模板是使用成员初值列(member initialization list)来初始化SmartPtr内类型为T的成员变量,并以类型为U的指针(由SmartPtr<U>持有)作为初值。前提条件:存在隐式转换,可以将U指针转换为T指针。也就是说,该构造函数只有在获得与其实参兼容类型时,才通过编译。

    member function template(成员函数模板)并不局限于构造函数,也经常应用于赋值操作。如shared_ptr,unique_ptr,weak_ptr的构造行为,以及除weak_ptr外的赋值操作(why?)。

    例如,TR1规范中shared_ptr的一份摘录,

    template<class T>
    class shared_ptr {
    public:
        template<class Y>
        explicit shared_ptr(Y* p); // 构造来自兼容的内置指针
    
        template<class Y>
        shared_ptr(shared_ptr<Y> const& r); // 构造来自兼容的shared_ptr. 泛化copy构造函数
    
        template<class Y>
        explicit shared_ptr(weak_ptr<Y> const& r); // 构造来自兼容的weak_ptr
    
        template<class Y>
        explicit shared_ptr(unique_ptr<Y>& r); // 构造来自兼容的unique_ptr
    
        template<class Y>
        shared_ptr& operator=(shread_ptr<Y> const& r); // assignment操作符, 来自兼容的shread_ptr
    
        template<class Y>
        shared_ptr& operator=(unique_ptr<Y>& r); // assignment操作符, 来自兼容的unique_ptr
        ...
    };
    

    上面所有的构造函数都是explicit,只有“泛化copy构造函数”除外,为什么?
    因为这样意味着某个shared_ptr类型隐式转换为另一个shared_ptr类型是被允许的,但从内置指针或其他智能指针类型进行隐式转换是不被认可的。不过,如果是显示转换比如cast强制转型,则是可以的。

    为什么传给构造函数、operator=的 unique_ptr参数,都并非const?
    因为条款13提到,复制unique_ptr,会导致原来的unique_ptr改变(指向null)。这是由unique_ptr的特性决定的:同一时刻,只允许一个unique_ptr指向一个给定对象。当把原来unique_ptr指向的对象,交由新unique_ptr指向时,原来的unique_ptr就指向了null。

    声明泛化copy构造函数和copy构造函数

    member template并不改变语言规则。条款5提到,编译器可能为我们生成4个成员函数:默认构造函数,copy构造函数和copy assignment操作符,析构函数。(注:C++11新增移动构造函数)

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

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

    小结

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

    [======]

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

    Define non-member functions inside template when type conversions are desired.

    本条款类同条款24。条款24讨论过:为什么只有non-member的函数才有能力“在所有实参身上实施隐式式类型转换”。本条款同样以Rational class的operator*为例。

    // 条款24的Rational的template版本
    template<typename T>
    class Rational {
    public:
        Rational(const T& numerator = 0, const T& denominator = 1); // 参数以passed by reference方式传递避免拷贝, 同时修改可以影响实参
        const T numerator() const; // 分子
        const T denominator() const; // 分母
        ...
    };
    
    // non-member函数重载operator*
    template<typename T>
    const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs)
    { ... }
    

    但与条款24示例不同的是,

    Rational oneHalf(1,2);
    Rational result = oneHalf * 2; // MSVC报错:没有与这些操作数匹配的 "*" 运算符

    同样是重载operator*,这里的例子为什么会报错?
    条款24内,编译器知道我们尝试调用什么函数(接受2个Rational参数的operator),但这里编译器却不知道我们想要调用哪个函数,因为它们想试图想出什么函数被名为operator的template具现化(产生)出来。而要具现化某个“名为operator并接受2个Rational参数”的函数,就必须先算出T是什么。问题是编译器不能做到。
    为了推动出T,先看operator
    调用动作的实参类型:Rational(oneHalf的类型)和int(数字2的类型)。2个参数分开考虑:
    1)以oneHalf(Rational)进行推导,operator*第一个参数被声明为const Rational,而传递的类型是Rational,所以T是int。
    2)以数字2(int)进行推导,由于template实参推导过程从不将隐式类型转换函数纳入考虑(也不考虑通过构造函数而发生的隐式类型转换),编译器并不能使用Rational的non-explicit构造函数将2转换为Rational。不过,这样的隐式转换却在函数调用过程中被使用,前提是在能调用一个函数前,首先必须知道那个函数存在。而为了知道它,必须先为相关function template推导出参数类型,然后才可以将适当的函数具现化出来。

    有一个简便方法:
    template class内friend声明式可以指涉某个特定函数。意味着class Rational可以声明operator是它的一个friend函数。class template并不依赖template实参推导(实参推导只施行于function template身上),所以编译器总是能在class Rational具现化时知道T类型。如果令Rational class声明适当operator为其friend函数,可以简化整个问题:

    template<typename T>
    class Rational {
    public:
        friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);
        // 由于在class template内, template名称(Rational)可以被用来作为“template和其参数”(Rational<T>)的简略表达式
        // 因此, 上面声明式等价于下面的声明式 <=>
        friend const Rational operator*(const Rational& lhs, const Rational& rhs) { // 使用简略表达式
            return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
        }
    };
    

    注意:如果将operator的实现放到class template外部(如.cpp文件),能通过编译,但无法链接。
    能通过编译原因:因为编译器通过模板函数operator
    ,知道我们要调用哪个函数。
    无法链接原因:不能在class template外部定义operator* template,必须在class内部定义。

    当然,我们可以通过operator* template调用一个定义在外部的函数doMultiply,来简化operator* 。

    template<typename T>
    const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs) 
    {
        return Rational(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);
        // 由于在class template内, template名称(Rational)可以被用来作为“template和其参数”(Rational<T>)的简略表达式
        // 因此, 上面声明式等价于下面的声明式 <=>
        friend const Rational operator*(const Rational& lhs, const Rational& rhs) { // 使用简略表达式
            return doMultiply(lhs, rhs);
        }
    };
    

    小结

    1)当我们编写一个class template,请将那些与template相关的、用于隐式类型转换的函数 定义为“class template内部的friend函数”。
    隐含2方面:class template的friend函数,内部实现。

    [======]

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

    Use traits clases for information about types.

    STL中,容器和算法是分开的,通过迭代器联系到一起。算法如何从迭代器类中萃取出容器元素的类型呢?
    这就需要用到traits class技术。

    在用traits class技术之前,先看下如何获得元素类型:

    template<typename IterT, typename DistT>
    void advance(IterT& iter, DistT d); // 函数模板:将迭代器iter移动d位置
    
    // 问题:如何得到IterT所指元素类型?
    // 如果IterT支持 随机访问(+=)操作,advance要做的事就是iter += d;
    // 如果不支持,advance要做的事就是 反复施行++iter(或--iter)一共d次。
    

    C++有5种常用迭代器:input(输入),output(输出),forward(前向),bidirectional(双向),random access(随机访问)。
    C++ STL分别提供专属卷标结构(tag struct)加以确认:

    struct input_iterator_tag
           {      // identifying tag for input iterators
           };
    struct output_iterator_tag
           {      // identifying tag for output iterators
           };
    struct forward_iterator_tag
           : input_iterator_tag
           {      // identifying tag for forward iterators
           };
    struct bidirectional_iterator_tag
           : forward_iterator_tag
           {      // identifying tag for bidirectional iterators
           };
    struct random_access_iterator_tag
           : bidirectional_iterator_tag
           {      // identifying tag for random-access iterators
           };
    

    5类迭代器详细介绍参见:C++Primer中文版5th Page366,或者 https://blog.csdn.net/CSDN_564174144/article/details/76231626

    迭代器如何得到IterT所指元素类型,并实现advance?
    一种方案是:

    template <typename IterT, typename DistT>
    void advance(Iter& iter, DistT d)
    {
        if (iter is a random access iterator) { // 只有random access iterator支持随机访问操作
            iter += d; // random access迭代器使用迭代器算术运算
        }
        else {
            if (d >= 0) { while(d--) ++iter; }
            else { while (d++) --iter; }
        }
    }
    

    这种做法首先必须判断iter是否为random access迭代器,即iter类型(Iter&)是否为random access迭代器。而要获得Iter代表的类型,我们可以通过traits class技术,在编译期就能获得。

    traits class技术

    traits class技术并不是C++关键字,而是C++程序员共同遵守的协议。其要求之一是:对内置(build-in)类型和用户自定义(user-defined)类型的表现必须一样好。
    具体来说,是将类型的traits(特征)信息放入一个template及其特化版本中,不同的容器模板实例化时,类型信息在不同特化版本中携带的traits信息不一样。

    利用迭代器萃取元素类型信息示例:

    // 通用版本
    template<class IterT>
    struct my_iterator_traits {
           typedef typename IterT::value_type value_type; // traits信息
    };
    // 偏特化版本
    template<class IterT>
    struct my_iterator_traits<IterT*> {
           typedef IterT value_type; // traits信息
    };
    void fun(int a) {
           cout << "func(int) is called" << endl;
    }
    void fun(double a) {
           cout << "func(double) is called" << endl;
    }
    void fun(char a) {
           cout << "func(char) is called" << endl;
    }
    
    // 客户端
    int main()
    {
           my_iterator_traits<vector<int>::iterator>::value_type a = 0;
           fun(a); // 打印"func(int) is called"
           my_iterator_traits<vector<double>::iterator>::value_type b = 1;
           fun(b); // 打印"func(double) is called"
           my_iterator_traits<vector<char>::iterator>::value_type c = 2;
           fun(c); // 打印"func(char) is called"
           return 0;
    }
    

    traits class技术简要介绍参见
    https://blog.csdn.net/lihao21/article/details/55043881

    小结

    1)traits class使得“类型相关信息”在编译期可用,使用template和template偏特化实现。

    [======]

    条款48:认识template元编程

    Be aware of template metaprogramming.

    元编程具体内容,暂略。

    小结

    1)Template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率;
    2)TMP可被用来生成“基于政策选择组合”(based on combinations of policy choices)客户定制代码,也可用来避免生成对某些特殊类型并不合适的代码。

    [======]

  • 相关阅读:
    第六周进度条
    团队开发需求分析视频连接
    敏捷开发综述
    数组2--数组首尾相接,求最大子数组
    数组1--求一个数的最大子数组
    第四周进度条
    四则运算3
    第三周进度条
    第二周进度条
    单元测试
  • 原文地址:https://www.cnblogs.com/fortunely/p/15648314.html
Copyright © 2011-2022 走看看