Effective C++ 条款44 将与参数无关的代码抽离template

1. template是节省时间和避免代码重复的有效方法,而且在将类模板(class template)具现化时,编译器只具现化那些被用到的成员函数,这更加节省了空间.

2. 正如两个函数存在代码重复时,倾向于把重复的代码抽出独立形成一个函数,然后让之前的函数调用这个函数,函数模板也可以这样,甚至类模板也可以采用相同的思想,例如,对于以下用于操作方矩阵的类模板:

template<typename T,std::size_t n> //n是非类型参数
class SquareMatrix{
public:
    void invert(){  //invert用来求方阵转置
        ...
    }
    ....
}

那么对于以下代码:

SquareMatrix<double,5> sm1;
...
sm1.invert();
SquareMatrix<double,6> sm2;
...
sm2.invert();

    具现化sm1和sm2时,尽管它们类型参数相同,都为double,但由于非类型参数不同,SquareMatrix会产生两份实体,包括其成员函数invert等,而它们的差别仅在于非模板参数n,这明显产生了大量重复的代码,初步改进方法如下:

template<typename T,size_t n>
class SquareMatrix:private SquareMatrixBase{
public:
void invert(){
this->invert(n);
}
...
}

    以上代码中,base class SquareMatrixBase含有一个带大小参数的invert,与之前SquareMatrix不同,它只对矩阵元素类型参数化,并不对矩阵大小参数化.因此对于同一类型参数,SquareMatrixBase只具现化出一份实体,结果就是类型参数相同,非类型参数不同的SquareMatrix虽然具现化出不同实体,但继承的是同一个SquareMatrixBase.

    此外,SquareMatrixBase中invert只是作为避免代码重复的一种手段,并不用来作为接口,因此设为protected;SquareMatrix对于invert的调用要加this->,否则编译器不会在SquareMatrixBase中查找invert(条款43);SquareMatrixBase只是用来帮助derived classes实现,并不是为了表现SquareMatrix和SquareMatrixBase之间的is-a关系,因而采用priva继承.

    到此为止,依然存在问题:SquareMatrixBase::invert从哪里取得要操作的方阵?采用为invert添加额外参数的方法可能是一个选择,但SquareMatrixBase可能还有其他参数需要获取矩阵,一劳永逸的方法就是在SquateMatrixBase内加一指针指向所操作的矩阵,由此启发,干脆吧矩阵尺寸也作为数据成员存储在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 class(SquareMatrix)决定内存分配方式,可以将矩阵存储在SquareMatrix对象内部:

template<typename T,size_t n>
class SquareMatrix:private SquareMatrixBase{
public:
    SquareMatrix():SquareMatrixBase<T>(n,data){}
    ...
private:
    T data[n*n];
}

如果方阵非常大,也可以将数据存储进堆内:

template<typename T,size_t n>
class SquareMatrix:private SquareMatrixBase{
public:
    SquareMatrix():SquareMatrixBase<T>(n,0),pData(new T[n*n]){
        this->setDataPtr(pData);
    }
    ...
private:
    boost::scoped_array<T> pData; 
}

(对于scoped_array,见条款13)

    到此为止,对于SquareMatrix的改进已经完成:SquareMatrix成员函数可以单纯地以inline方式调用base class版本,后者由"持有同种元素"(不论矩阵大小)之所有矩阵共享;同时,不同大小的SquareMatrix对象有着不同类型,因此即使(例如SquareMatrix<double,5>和SquareMatrix<double,6>)对象使用相同的SquareMatrixBase<double>成员函数,也无法传递一个SquareMatrix<double,5>对象到一个期望获得SquareMatrix<double,6>的对象去.

3. 从效率上来看,最初带有大小参数的SquareMatrix类模板(简称为"尺寸专属版")的invert版本可能产生比共享版本(尺寸以参数传递或存储在对象内)更佳的代码."例如在尺寸专属版中,尺寸是个编译期常量,因此可藉由常量的广传达到最优化,包括把它们折进被生成指令中成为直接操作数".这在共享版本中无法做到.

    另一方面,对于共享版本,不同大小的矩阵只拥有单一版本的invert,这可减少执行文件大小,也就因此降低程序的working set(在一个"虚内存环境下"执行的进程所使用的哪一组内存页)大小,并强制指令高速缓存区内的引用集中化,这些都可能使程序进行地更快捷.

    从空间上来看,每个共享版本的SquareMatrix对象都有一个指针指向要操作的矩阵,这使每一个对象增加了一个指针的大小.当然也可以拿掉这些指针,将SquareMatrixBase的pData访问级别设为protected,但这会降低封装性,也有可能造成空间管理的混乱和复杂.

4. 以上只是讨论了非类型模板参数(non-type template parameter)带来的膨胀,其实类型模板参数(type parameter)也会导致膨胀,例如许多平台上int和long有相同的二进制表述,所以vector<int>和vector<long>可能完全相同,某些template就有可能被具现化为int和long两个版本.类似情况,大多数平台上,所有指针类型有相同的二进制表述,因此template持有指针者(例如list<int*>,list<const int*>,list<SquareMatrix<long,3>*>等等)常常应该为每一份成员函数提供唯一一份被不同类型参数共享的底层实现.也就是说,如果某些成员函数操作强类型指针(T*)等,应该令它们调用另一个操作无类型指针void*的函数,由后者完成实际工作.某些C++标准程序库的实现版本的确为vector,list等templates做了这件事.

 

posted @ 2015-09-11 18:27  Reasno  阅读(448)  评论(0编辑  收藏  举报