Effective C++——条款44(第7章)

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

Factor parameter-independent code out of templates.

    Templats是节省时间和避免代码重复的一个很好的办法
.不再需要键入20个类似的 class 而每一个都带有15个成员函数,只需键入一个 class template,留给编译器去具现化那20个需要的相关的 class 和300个函数.Function template 有类似的诉求.
    但是,如果不小心,使用 template 可能会导致代码膨胀:其二进制带着重复的代码,数据,或两者.其结果有可能源码看起来合身而整齐,但目标码却不是那么回事.因此需要知道如何避免这样的二进制浮夸. 主要的工具是:共性与变性分析(commonality and variablity analysis).
    当编写某个函数,发现某些部分的实现码和另一个函数的实现码实质相同,会单纯地重复这些码吗?当然不,会抽出两个函数的共同部分,把它们放进第三个函数中,然后令原先两个函数调用这个新函数.也就是数,分析了这两个函数,找出共同的部分和变化的部分,把共同部分搬到一个新函数去,保留变化的部分在原函数中不动.同样道理,如果正在编写某个 class,而明白某些部分和另一个 class 的某些部分相同,也不会重复这共同的部分.取而代之的是把共同部分搬移到新 class 去,然后使用继承或复合(详见 条款32, 条款38, 条款39),令原先的 class 取用这共同特性.而原 class 的互异部分仍然保留原位置不动.
     编写 template 时,也是做相同的分析,以相同的方式避免重复,但其中有个窍门.在non-template 代码中,重复十分明确:可以"看"到两个函数或两个 class 之间有重复.然而在 template 代码中,重复是隐晦的:毕竟只存在一份 template 源码,所以必须去感受当 template 被具现化多次时可能发生的重复.
      例如,假设想要为固定尺寸的正方矩阵编写一个 template,该矩阵的性质之一是支持逆矩阵运算:
template <typename T, std::size_t n>
class SquareMatrix {
public:
    ...
    void invert();
};
    这个 template 接受一个类型参数T,除此之外还接受一个类型为 size_t 的参数,那是个非类型参数(non-type parameter).它们完全合法.
    现在,考虑下面的代码:
SquareMatrix<double, 5> sm1;
...
sm1.invert();                    // 调用SquareMatrix<double, 5>::invert
SquareMatrix<double, 10> sm2;
...
sm2.invert();                    // 调用SquareMatrix<double, 10>::invert
    这会具现化两份invert,这些函数并非完全相同,因为其中一个操作的5*5矩阵而另一个是10*5, 但除了常量5和10,两个函数的其他部分完全相同.这是 template 引出代码膨胀的一个典型例子.
    如果看到两个函数完全相同,只除了一个使用5另一个使用10,会怎么做?本能会为它们建立一个带数值参数的函数,然后以5和10来调用这个带参数的函数,而不重复代码.下面是对SquareMatrix的第一次修改:
template <typename T>
class SquareMatrixBase {
protected:
    ...
    void insert(std::size_t matrixSize);
    ...
};
template <typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T> {
private:
    using SquareMatrixBase<T>::invert;        // 避免这样base版本的invert
public:
    ...
    void insert() { this-invert(n); }        // 调用base class版本的invert
};
    带参数的invert位于base class SquareMatrixBase中.和SquareMatrix一样,SquareMatrixBase也是个 template, 不同的是它只是"矩阵元素对象的类型"参数化,不对矩阵的尺寸参数化.因此对于所有给定的元素对象类型,所有矩阵共享一个SquareMatrixBase class.它们也将因此共享这唯一一个 class 内的invert.
    SquareMatrixBase::invert只是企图成为"避免derived class代码重复"的一种方法,所以 它以 protected 替换 public.调用它而造成的额外成本应该是0,因为derived class 的invert调用base class 版本时用的是 inline 调用(这里的 inline 是隐晦的,详见 条款30). 这些函数使用"this->"记号,因为若不这样做,便如 条款43所述,模板化基类(templatized base class,例如SquareMatrixBase<T>)内的函数名称会被derived class 遮掩.注意,SquareMatrix和SquareMatrixBase之间的 继承关系是 private.这反应一个事实:这里的base class 只是为了帮助derived class 实现,不是为了表现SquareMatrix和SquareMatrixBase之间的is-a关系(关于 private 继承,详见 条款39).
    目前为止一切都好,但还有一些棘手的问题没有解决.SquareMatrixBase::invert如何知道该操作什么数据?虽然它从参数中知道矩阵尺寸,但它如何知道哪个特定矩阵的数据在哪里?想必只有derived class 知道.其中一个办法是令SquareMatrixBase贮存一个指针,指向矩阵数值所在的内存.而只要它存储了那些东西,也就可能存储矩阵尺寸,成果看起来像这样:
template <typename T>
class SquareMatrixBase {
protected:
    SquareMatrixBase(std::size_t n, T* pMem) :size(n), pData(pMem)
    {}            // 存储矩阵大小和一个指针,指向矩阵数值.重新赋值给pData
    void setDataPtr(T* ptr) { pData = ptr; }    // 重新赋值给pData
    ...
private:
    std::size_t size;
    T* pData;
};
    这允许derived class 决定内存分配方式,某些实现版本也许会决定将矩阵数据存储在SquareMatrix对象内部:
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(也就是通过 new 来分配内存):
template <typename T, std::size_t n>
class SquareMatrix : private SquareMatrixBase<T> {
public:
    SquareMatrix() : SquareMatrixBase<T>(n, NULL), pData(new T[n * n])
    { this->setDataPtr(pData.get()); }    // 将base class的数据指针设置为NULL
    ...
private:
    boost::scoped_array<T> pData;    // 关于boost::scoped_array,详见条款13
};
    不论数据存储于何处,从膨胀的角度来看,SquareMatrix成员函数可以单纯地以 inline 方式调用base class 版本,后者由"持有相同类型元素"(不论矩阵大小)的所有矩阵共享.
    看上去很好,但必须付出代价,硬是绑着矩阵尺寸的那个invert版本,有可能生成比共享版本更佳的代码.例如在尺寸专属版,尺寸是个编译期常量,因此可以借由常量的广传达到最优化,包括把它们折进被生成指令中成为直接操作数.这在"与尺寸无关"的版本中是无法办到的.
    从另一个角度看,不同大小的矩阵只拥有单一版本的invert,可减少执行文件大小,也就因此减低程序的working set大小,并强化指令高速缓存区内的引用集中化,这些都可能使程序执行得更快速,超越"尺寸专属版"invert的最优化效果.
    所谓的working set是指对一个在"虚内存环境"下执行的进程而言,其所使用的那一组内存页.
    这些条款只讨论由non-type template parameters(非类型模板参数)带来的膨胀,其实type parameters(类型参数)也会导致膨胀.在大多数平台上,所有指针类型都有相同的二进制表述,因此凡templates持有指针者(例如list<int *>, list<const int*>, list<SquareMatrix<long, 3> *>等等)往往应该对每一个成员函数使用唯一一份底层实现.这很具代表性地意味,如果实现某些成员函数而它们操作强型指针(strongly typed pointers,即T *),应该令它们调用另一个操作无类型指针(即 void *)的函数,由后者完成实际工作.
     注意:
    template 生成多个 class 和多个函数,所以任何 template 代码都不该与某个造成膨胀的 template 参数产生相依关系.
    因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或 class 成员变量替换 template 参数.
    因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representation)的具现类型(instantiation type)共享实现码.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值