<Effective C++>读书笔记-8

资料摘自<Effective C++>

条款44:将与参数无关的代码抽离templates
  Templates是节省时间和避免代码重复的一个奇方妙法。不同需要键入20个类似的classes而每一个带有15个成员函数,你只需键入一个class template,留给编译器去具现化那20个你需要的相关class和300个函数。Function templates有类似的诉求。替换写许多函数,你只需写一个function template,然后让编译器做剩余的事情。
  你的主要工具有个气势恢宏的名称:共性与变性分析(commonality and variability analysis),但其概念十分平民化。纵使你从未写过一个template,你始终做着这样的分析。
  当你编写某个函数,而你明白其中某些部分的实现码和另一个函数的实现码实质相同,你会很单纯地重复这些码吗?当然不。你会抽出两个函数的共同部分,把它们放进第三个函数中,然后令原先两个函数调用这个新函数。也就是说,你分析了两个函数,找出共同的部分和变化的部分,把共同部分搬到一个新函数去,保留变化的部分在原函数中不动。同样道理,如果你正在编写某个class,而你明白其中某些部分和另一个class的某些部分相同,你也不会重复这共同的部分。取而代之的是你会把共同部分搬移到新class去,然后使用继承或复合令原先的class取用这共同特性。而原class的互异部分仍然留存原位置不动。
  编写templates时,也是做相同的分析,以相同的方式避免重复,但其中有个窍门。在non-template代码中,重复十分明确:你可以“看”到两个函数或两个classes之间有所重复。然而在template代码中,重复是隐晦的:毕竟只存在一份template源码,所以你必须训练自己去感受当template被具现化多次时可能发生的重复。
  举个例子,假设你想为固定尺寸的正方矩阵编写一个template。该矩阵性质之一是支持逆矩阵运算。
template<tyname T, std::size_t n>
class SquareMatrix{ //template支持n*n矩阵
public:
...
void invert(); //求逆矩阵
};
考虑下面代码:
SquareMatrix<double,5> sm1;
...
sm1.invert();
SquareMatrix<double,10> sm2;
...
sm2.invert();
这会具现化两份invert。这些函数并非完完全全相同,因为其中一个操作的是5*5矩阵而另一个操作的是10*10矩阵,但除了常量5和10,两个函数的其他部分完全相同。这是template引出代码膨胀的一个典型例子。
  如果你看到两个函数完全相同,只除了一个使用5而另一个使用10,你会怎么做?你的本能会为它们建立一个带数值参数的函数,然后以5和10来调用这个带参数的函数,而不重复代码。你的本能很好,下面是对SquareMatrix的第一次修改:
tempalte<typename T>
class SquareMatrixBase{
protected:
...
void invert(std::size_t matrixSize);
...
};
tempalte<typename T,std::size_t n>
class SquareMatrix:private SquareMatrixBase<T>{
private:
using SquareMatrixBase<T>::invert; //避免遮掩base版的invert
public:
...
void invert() {this->invert(n);}
};
就如你所看到,带参数的invert位于base class SquareMatrixBase中。和SquareMatrix一样,SquareMatrixBase也是个template,不同的是它只对“矩阵元素对象的类型”参数化,不对矩阵的尺寸参数化。因此对于某给定之元素对象类型,所有矩阵共享同一个SquareMatrixBase class。它们也将因此共享这唯一一个class内的invert。
  SquareMatrixBase::invert只是企图成为“避免derived class代码重复”的一种方法,所以它以protected替换public。调用它造成的额外成本应该是0,因为derived classes的invert调用base class版本时用的是inline调用。这些函数使用“this->”记号,因为若不这样做,模板化基类内的函数名称会被derived classes掩盖。请注意SquareMatrix和SquareMatrixBase之间的继承关系是private。这说明这里的base class只是为了帮助derived classes实现,不是为了表现SquareMatrix和SquareMatrixBase之间的is-a关系。
  目前为止一切都好,但还有一些棘手的题目没有解决。SquareMatrixBase::invert如何知道该操作什么数据?虽然它从参数中知道矩阵尺寸,但它如何知道哪个特定矩阵的数据在哪儿?想必只有derived class知道。Derived class如何联络其base class做逆运算动作?一个可能的做法是为SquareMatrixBase::invert添加另一个参数,也许是个指针,指向一块用来放置矩阵数据的内存起始点。那行得通,但十之八九invert不是唯一一个可写为“形式与尺寸无关并可移至SquareMatrixBase内”的SquareMatrix函数。如果有若干这样的函数,我们唯一要做的就是找出保存矩阵元素值的那块内存。我们可以对所有这样的函数添加一个额外参数,却得一次又一次地告诉SquareMatrixBase相同的信息,这样似乎不好。
另一个办法是令SquareMatrixBase贮存一个指针,指向矩阵数值所在的内存。而只要它存储了那些东西,也就可能存储矩阵尺寸。成果看起来像这样:
tempalte<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对象内部:
tempalte<typename T, std::size_t n>
class SquareMatrix:private SquareMatrixBase<T>{
public:
SquareMatrix():SquareMatrixBase<T>(n, data) {}
...
private:
T data[n * n];
};
这种类型的对象不需要动态分配内存,但对象自身可能非常大。另一种做法是把每一个矩阵的数据放进heap(也就是通过new来分配内存):
tempalte<typename T, std::size_t n>
class SquareMatrix:private SquareMatrixBase<T>{
public:
SquareMatrix():SquareMatrixBase<T>(n, 0),pData(new T[n * n]){
this->setDataPtr(pData.get());
}
...
private:
boost::scoped_array<T> pData;
};
不论数据存储于何处,从膨胀的角度检讨之,关键是现在许多--说不定是所有--SquareMatrix成员函数可以单纯地以inline方式调用base class版本,后者由“持有同型元素”之所有矩阵共享。在此同时,不同大小的SquareMatrix对象有着不同的类型,所以即使对象使用相同的SquareMatrixBase<double>成员函数,我们也没机会传递一个SquareMatrix<double, 5>对象到一个期望获得SquareMatrix<double, 10>的函数去。
  这种做法必须付出代价。硬是绑着矩阵尺寸的那个invert版本,有可能生成比共享版本更佳的代码。例如在尺寸专属版中,尺寸是个编译期常量,因此可以藉由常量的广传达到最优化,包括把它们折进被生成指令中成为直接操作数。这在“与尺寸无关”的版本中是无法办到的。
  从另一角度看,不同大小的矩阵只拥有单一版本的invert,可减少执行文件大小,也就因此降低程序的working set大小,并强化指令调整高速缓存区内的引用集中化。这些都可能使程序执行得更快速,超越“尺寸专属版”invert的最优化效果。哪一个影响占主要地位?欲知答案,唯一的办法是两者都尝试并观察你的平台的行为以及面对代表性数据组时的行为。
  另一个效能评比所关心的主题是对象大小。如果你不介意,可将前述“与矩阵大小无关的函数版本”搬至base class内,这会增加每一个对象的大小。例如在刚才展示的例子中,每一个SquareMatrix对象都有一个指针指向SquareMatrixBase class内的数据。虽然每个derived class已经有一种取得数据的办法,这会对每一个SqureMatrix对象增加至少一个指针那么大。当然也可以修改设计,拿掉这些指针,但是再一次,这其中需要若干取舍。例如令base class贮存一个protected指针指向矩阵数据,会导致丧失封装性。也可能导致资源管理上的混乱和复杂;是的,如果base class存储一个指针指向矩阵数据,那些数据空间也许是动态分配获得,也许存储于derived class对象内,如何判断这个指针该不该被删除呢?这样的问题有其答案,但你愈是尝试精密的做法,事情变得愈是复杂。从某个角度看,一点点代码重复反倒看起来有点幸运了。
  这个条款只讨论由non-type template parameters(非类型模板参数)带来的膨胀,其实type parameters(类型参数)也会导致膨胀。例如在许多平台上int和long有相同的二进制表述,所以像vector<int>和vector<long>的成员函数有可能完全相同--这正是膨胀的最佳定义。某些器(linkers)会合并完全相同的函数实现码,但有些不会,后者意味某些templates被具现化为int和long两个版本,并因此造成代码膨胀。类似情况,在大多数平台上,所有指针类型都有相同的二进制表述,因此凡templates持有指针者往往应该对每一个成员函数使用唯一一份底层实现。这很具代表性地意味,如果你实现某些成员函数而它们操作强型指针(strongly typed pointers,即T*),你应该令它们调用另一个操作无类型指针(untyped pointers,即void*)的函数,由后者完成实际的工作。某些C++标准程序库实现版本的确为vector,deque和list等templates做了这件事。如果你关心你的templates可能出现代码膨胀,也许你会想让你的templates也做相同的事情。
请记住:
Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。
因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同二进制表述(binary representations)的具现类型(instantiation types)共享实现码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值