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

[======]

posted @ 2021-12-06 08:55  明明1109  阅读(268)  评论(0编辑  收藏  举报