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

template是节省时间和避免重复代码的一个奇妙方法。class template的成员函数只有在被使用时才被暗中具现化。function templates有类似的诉求。

但是如果你不小心,使用templates可能导致代码膨胀(code bloat):其二进制代码带着重复(或几乎重复)的代码、数据、或两者。其结果可能源码看起来合身整齐,但目标码却不是那么回事。你需要知道如何避免这样的二进制浮夸。

主要工具是:共性与变性分析。

在non-template中,重复十分明确,然而在template中,重复是隐晦的:你必须训练自己去感受当template被具现化多次时可能发生的重复。

举个例子,矩阵template,支持矩阵inversion运算。

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, 10> sm2;
sm2.invert();

这会具现两份invert。这些函数并非完全相同,但除了常量5和10,其他部分都相同,这是template引出代码膨胀的一个典型例子。

下面是对SquareMatrix的第一次修改:

template<typename T>                //与尺寸无关的base class
class SquareMatrixBase{
protected:
    void invert(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 invert() {this->invert(n);}    //制造一个inline调用,用this->为了不被derived classes的函数名称掩盖
};

带参数的invert位于base class中。和SquareMatrix一样,也是个template,不同的是他只对“矩阵元素对象的类型”参数化,不对矩阵的尺寸参数化。因此对于给定的元素对象类型,所有矩阵共享同一个(也是唯一一个)SquareMatrixBase class。也将因此而共享这唯一一个class内的invert。

目前为止一切都好,但是SquareMatrixBase::invert如何知道该操作什么数据?想必只有derived class知道。一个可能的做法是为SquareMatrixBase::invert添加一个新的参数,也许是个指针,指向一块用来放置矩阵数据的内存起始点。那行的通,但是十之八九invert不是唯一一个可写为“形式与尺寸无关并可移至SquareMatrixBase内”的SquareMatrix函数。如果有若干这样的函数,我们可以对所有这样的函数添加一个额外参数,却得一次次地告诉SquareMatrixBase相同的信息,这样不好。

另一个办法是令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;
};

这允许derived classes决定内存分配方式。某些实现版本也许会将矩阵数据存储在SquareMatrix对象内部:

template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T>{
public:
    SquareMatrix()
        : SquareMatrixBase<T>(n, data){}

private:
    T data[n*n];

}

另一种做法是把每个矩阵的数据放进heap:

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());//将pData的一个副本交给base class
    }
private:
    boost::scoped_array<T> pData;
};

现在SquareMatrix<double, 5>和SquareMatrix<double, 10>有着不同的类型,即使使用相同的SquareMatrixBase<double>成员函数。

是的,很棒,但是必须付出代价。硬是绑着矩阵尺寸的那个invert版本,有可能生成比共享版本(其中尺寸乃以函数参数传递或存储在对象内)更佳的代码。例如在尺寸专属版本中,尺寸是个编译期常量,因此可以藉由常量的广传达到最优化,包括把它们折进被生成指令中成为直接操作数。这在“与尺寸无关”的版本中无法办到。

另一个角度看,不同大小大的矩阵只拥有单一版本的invert,可减少执行文件大小,因此降低working set(虚内存环境下执行进程所使用的那一组内存页)大小。并强化指令高速缓存区内的引用集中化(locality of reference)。这些都可能使程序执行的更快速,超越“尺寸专属版”invert的最优化效果。

另一个所关心的主题是对象的大小。如果将前述“与矩阵大小无关的函数版本”搬至base class内,这会增加每个对象的大小。每个SquareMatrix对象都有一个指针指向SquareMatrixBase class内的数据。虽然每个derived class已经有一种取得数据的办法,这会对每一个SquareMatrix对象至增加少一个指针那么大。当然可以拿掉这些指针,但是这其中需要若干取舍。

其实type parameter(类型参数)也会导致膨胀。例如许多平台上int和long有相同二进制表述,所以像vector<int>和vector<long>的成员函数有可能完全相同——这正是膨胀的最佳意义。某些链接器会合并完全相同的函数实现码,但有些不会,后者意味着某些templates被具现化为int和long两个版本,并因此造成代码膨胀。类似情况,大多数平台下,所有指针类型都有相同的二进制表述,因此凡是templates持有指针者(例如list<int*>,list<const int*>,list<SquareMatrix<long,3>*>等等)往往应该对每一个成员函数使用唯一一份底层实现。这很具代表性的意味,如果你实现某些成员函数而他们操作强型指针(即 T*),你应该令他们调用另一个操作无类型指针(即 void*)的函数,而后者完成实际工作。某些c++标准程序库实现版本的确为vector,deque,list等templates做了这件事。如果你关心你的templates可能出现代码膨胀,也许你会想让你的templates也做相同的事情。

template生成多个class和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。

非类型模板参数造成的代码膨胀,往往可以消除,做法是以函数参数或成员变量替换template参数。

类型参数造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述的具现类型共享实现码。

posted @ 2012-02-15 20:50  lidan  阅读(499)  评论(0编辑  收藏  举报