ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Effective C++读书笔记~7 模板与泛型编程

2021-12-06 09:04:32  阅读:205  来源: 互联网

标签:const Effective 读书笔记 C++ ptr template SmartPtr class 构造函数


目录

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

[======]

标签:const,Effective,读书笔记,C++,ptr,template,SmartPtr,class,构造函数
来源: https://www.cnblogs.com/fortunely/p/15648314.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有