一、代码重复与对应策略
对于函数而言
- 假设我们编写某些函数(不止一个),这些函数当有有部分代码都是相同的,因此我们认为这份代码对于程序来说是重复的
- 那么此时我可以将这些函数共同的部分抽取出来,然后放在另一个函数中,让原本那些函数调用这个新函数。这样就达到了缩减代码的效果
对于类而言
- 假设我们在编写一组类,而这些类中有些部分都是相同的,因此我们认为这份代码对于程序来说是重复的
- 那么此时我们可以将这些类中共同的部分抽取出来,建立一个新类,然后让原本那些类继承于这个新类。这样就达到了缩减代码的效果
对于template(函数模板/类模板)来说
- 我们知道只有当template被使用时才会进行代码的实例化,并且针对每一个实例化,在代码中都会生成相对应的代码,因此针对于每一个实例化,它们之间也可能会产生代码重复的现象
- 对于普通函数/类来说,代码重复是很容易看出来的(通过代码)。但是对于template来说,其是不容易发现的,因为只有template被实例化时,你才知道template产生了什么内容
二、演示案例
- 假设现在我们有这样一个类模板,其用来表示固定尺寸的正方矩阵,该矩阵还支持一个逆矩阵运算的方法。代码如下:
//矩阵的元素类型为T,矩阵的大小为n*n类型(模板第二个类型为非类型参数)
template<typename T, std::size_t n>
class SquareMatrix {
public:
void invert(); //求逆矩阵
};
- 假设我们有下面的调用,那么对于invert()函数的调用来说可能会产生代码膨胀。理由如下:
- 下面的5*5矩阵和10*10矩阵都调用了invert(),该函数是用来对矩阵进行逆转的
- 但是根据invert()的实现代码来看,其除了矩阵的大小不一样以外,invert()其他的代码对于矩阵的处理都是相同的,因此上面生成的两个模板会产生代码重复的地方
SquareMatrix<double,5> sm1;
sm1.invert();
SquareMatrix<double,10> sm2;
sm2.invert();
第一次修改
- 因为invert()中的代码会产生重复,因此我们现在建立下面的继承体系。代码如下:
- 我们修改了SquareMatrix类模板名为SquareMatrixBase,使其只接受矩阵类型,为invert()添加一个参数,用来表示来处理的矩阵大小
- 添加一个SquareMatrix类模板继承于它,让其接受一个size_t模板参数用来表示处理的矩阵的大小
//不论SquareMatrix产生多少粉,SquareMatrixBase在代码中只产生一份 template<typename T> 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; //避免派生类隐藏基类的invert函数 public: void invert() { this->invert(n); //为什么使用this,参阅条款43 } };
- 此处相比于起初节约代码的原因:
- 当SquareMatrix实例化时,其并不会产生太多的代码,因为其直接调用基类中的invert函数
- 并且由于基类中的invert不会因为矩阵大小不同而实例化不同的模板类了,因为其生成一份模板类代码,根据矩阵大小的不同,调用其中的invert()函数即可
- 一些语法注意事项:
- 因为SquareMatrixBase的功能交给派生类实现了,因此将SquareMatrixBase中的invert()函数声明为protected,不允许外界直接操作SquareMatrixBase
- SquareMatrix使用private继承于SquareMatrixBase,因为其用到的技术是前面介绍过的“implemented-terms-of(根据某物实现出)”的技术(可以参阅条款38、39)
- 另外,在SquareMatrix的invert()中对于基类invert()的调用使用了this指针(这一块内容可以参阅条款43)
第二次修改
- 现在还有问题:
- SquareMatrixBase()用来完成对矩阵的处理,但是SquareMatrixBase如何知道处理的这些数据来自于哪里呢?
- 因此SquareMatrix()必须使用一种方法,将所操作的数据传递给SquareMatrixBase()
- 解决方法:
- ①(不可取的方法):为SquareMatrixBase()::invert()提供一个指针参数,用来表示要处理的矩阵的数据。这样行的通,但是不可取,因为SquareMatrixBase()整个类都需要操作这块数据,如果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; } //更改pData的数据 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) {} //将存储矩阵数据的指针传递给基类 private: T data[n*n]; //存储矩阵的实际数据 };
第三次修改
- 紧接着“第二次修改”,上面我们将矩阵数据存储在SquareMatrix类中可能会造成SquareMatrix对象自身非常大
- 另一种做法是:把矩阵的数据存储在堆上。代码如下:
template<typename T, std::size_t n> class SquareMatrix :private SquareMatrixBase<T> { public: SquareMatrix(): SquareMatrixBase<T>(n, 0), pData(new T[n*n]) { //将它的一个副本交给base class this->setDataPtr(pData.get()); } private: boost::scoped_array<T> pData; };
- 经过三次修改,代码已经相对于比较优化了,具体细节在于:
- SquareMatrix的成员函数会单纯地以inline方式调用SquareMatrixBase版本
- SquareMatrixBase类由“持有相同元素类型但矩阵大小不同的”SquareMatrix类对象所共享
- 并且我们为SquareMatrixBase添加了一个setDataPtr()函数,所以当我们操作不同的SquareMatrix对象时(例如SquareMatrix<double,5>和SquareMatrix<double,10>),不会担心某个对象会操作另外一个对象的数据,因为我们使用了setDataPtr()设置了SquareMatrixBase中的矩阵数据指针
- 关于代码与效率的几个说明:
- ①SquareMatrix的invert()函数,可能比共享版本(SquareMatrixBase的)的invert()函数产生更佳的代码。例如在SquareMatrix中,矩阵大小是个编译器常量,因此可以藉由常量的广传达到最优化,包括把它们折进被生成指令中成为直接操作数。这在SquareMatrixBase中是无法办到的
- ②不同大小的矩阵只拥有单一版本的invert()(SquareMatrixBase中的),可减少执行文件大小,也就因此降低程序的work set大小,并强化指令告诉缓存区内的引用集中化。这些都可能使程序执行得更快速
- 所谓work set是指对一个在“虚内存环境”下执行的进程而言,其所使用的那一组内存页
- ③另一个效能评比所关心的主题是对象大小:我们在SquareMatrix中存储了一个指针,指向于矩阵的数据,因此SquareMatrix锁创建的对象大小会增加。
- 另一种做法:去除SquareMatrix中的指针,让SquareMatrixBase存储一个protected指针指向矩阵数据。但是这会导致丧失封装性,另外可能导致资源管理上的混乱和复杂(参阅条款22)
- 因此,我们在SquareMatrix存储指针,虽然增加了一些空间,但是从利与弊来说,这是值得的
三、附加
- 本条款是讨论由non-type template parameters(非类型模板参数)带来的膨胀,其实type parameters(类型参数)也会导致膨胀
例如
- 许多平台上int和long有着相同的二进制表述
- 所以vector<int>和vector<long>的成员函数有可能完全相同——造成代码重复/膨胀
- 有些链接器(linkers)会合并相同的函数实现码,但有些不会
- 因此某些模板被具体化为int和long两个版本,那么就造成代码重复/膨胀
例如
- 在大多数平台上,所有指针类型都有着相同的二进制表述
- 因此如果一个templates持有指针,例如list<int*>、list<const int*>、list<SquareMatrix<long,3>*>等等,往往应对每一个成员函数使用唯一一份底层实现
- 折痕具代表性地意味,如果你实现某些成员函数而它们操作强型指针(即T*),你应该令它们调用另一个操作无类型(即void*)指针的函数,由后者完成实际工作
- 某些C++标准程序库实现版本的确为vector、deque、list等templates做了这些事情。如果你关心你的templates可能出现代码膨胀,也会你会想让你的templates也使用这些技术
四、总结
- Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系
- 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数
- 因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述的具现类型共享实现码