模板与泛型编程

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

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

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

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

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

  template参数身上的隐式接口,就像class对象身上的显示接口一个样真实,二者都在编译期完成检查。无法在template中使用“不支持template所要求之隐式接口”的对象,代码编译通不过。

请记住:

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

  iter的类型是C::const_iterator,实际是什么必须取决于template参数C。

  1. template内出现的名称如果相依于某个template参数,称之为从属名称
  2. 如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称

  C::const_iterator就是这样一个名称。实际上它还是个嵌套从属类型名称,也就是个嵌套从属名称并且指涉某类型。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<>      // 一个全特化,template MsgSender针对类型CompanyZ特化了,且特化是全面性的,也就是说类型一旦被定义为CompanyZ,再也没有其他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声明式

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

  但如果被调用的是virtual函数,上述的明确资格修饰会关闭virtual绑定行为。

  从名称可视化的角度出发,对编译器承若“base class template”的任何特化版本都支持一般(泛化)版本所提供的接口,

请记住:

  1. 可在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;

  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,因为我们需要此泛化构造函数支持隐式转换。

  然而,此泛化构造函数提供的东西比我们需要的更多。我们需要根据一个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构造函数
        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*支持了混合式操作(通过隐式转换函数完成)。

请记住:

  1. 当我们编写一个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)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。

posted on 2020-02-14 20:00  tianzeng  阅读(325)  评论(0编辑  收藏  举报

导航