前言
Templates是避免代码重复与节约时间的最优解。但有时,template可能会造成代码膨胀:二进制码带着大量重复(或几乎重复)的代码或者数据。其源码可能看起来优雅整洁,但实际上生成的object code完全不是一回事,我们需要对此进行优化。
共性与变性分析
当我们在撰写函数时,往往倾向于提取出函数的共同部分,组成新的函数,然后令原来的函数调用该新函数。在OOP中,如果多个class存在相同部分,则会抽离classes的共有部分组成新的class,然后采用继承或者复合获取共性,而原本classes的互异部分则保持不变。
在编写templates也是同样的方式避免重复,但在细节处略有不同。在non-template程序中我们可以明确地看到重复的函数或重复的class成员,但在template编码中,源码只有一份,必须自己去感知当template具现化时可能发生的重复。
实例分析
为一个方阵编写template,其性质之一是支持求逆:1
2
3
4
5template<typename T,size_t n>
class SquareMatrix{
public:
void invert();//求逆矩阵
}
假设现有操作如下:1
2
3
4SquareMatrix<double,5> sm1;
sm1.invert();
SquareMatrix<double,10> sm2;
sm2.invert();
以上操作直接导致具现化了两个invert,虽然它们并非完全相同,但是相当类似,唯一的区别就是一个作用于5*5矩阵一个作用于10*10矩阵。
解决方案
第一版修改方案
当我们看到两个除了参数之外完全相同的函数,本能反应应当是建立一个带数值参数的可调用函数,而不是在class中重复代码。基于这种理念,第一版修改方案如下:1
2
3
4
5
6
7
8
9
10
11
12template<typename T>
class SquareMatrixBase{
protected:
void invert(size_t matrixSize);
}
template<typename T,size_t n>
class SquareMatrix:private SquareMatrixBase{
private:
using base::invert;//避免遮蔽名称
public:
void invert() {this->invert(n);}//inline调用
}
如你所见,所有矩阵元素类型相同的class共有一个bc,并且共享这唯一一个class内的invert.在dc中调用invert的额外成本应该为0,因为这是一个inline调用,this->的使用详见前一节内容(Effective C++ 44)。private继承说明了bc只是为了帮助dc实现,他们并不是is-a的关系。
第二版修改方案
现在的问题是:base内的invert如何知道操作什么数据?虽然它知道尺寸,但他如何知道某个特定矩阵的数据存在于内存的何处?这显然只有dc知道。dc如何联络bc做逆运算呢?
一个可能的做法是向invert函数添加另一个参数,一个指向矩阵数据的指针。根据经验,invert应该不是唯一一个需要获取矩阵数据的函数,所以我们令base class贮存一个指针,指向矩阵数值指向的内存,既然它持有了数据,那必然也需要了解矩阵的大小。具体实现如下所示:1
2
3
4
5
6
7
8
9template<typename T>
class SquareMatrixBase{
protected:
SquareMatrixBase(size_t n,T *pMem):size(n),pData(pMem) {}
void setDataPtr(T* ptr) {pData = ptr;}//更改数据域
private:
size_t n;
T* pData;//矩阵具体内容
}
这种写法允许derived classes自主决定内存分配方式。
dc的内存分配方式
- 矩阵存储于对象内部这一类对象无需动态分配内存,但是对象自身可能非常大。
1
2
3
4
5
6
7template<typename T,size_t n>
class SquareMatrix:private SquareMatrixBase<T>{
public:
SquareMatrix():SquareMatrixBase<T>(n,data) {}
private:
T data[n*n];
}; - 把对象数据放入heap
1
2
3
4
5
6
7
8template<typename T,size_t n>
class SquareMatrix:private SquareMatrixBase<T>{
public:
SquareMatrix():SquareMatrixBase<T>(n,0),padta(new T[n*n])
{this->setDataPtr(pData.get());}
private:
shared_ptr<T> pData;//RAII
};
总结
- template生成多个class与多个函数,所以任何template代码都不应该与某个造成膨胀的template参数相互依存
- 因为non-type template parameters造成的代码膨胀往往可以消除,做法是以函数参数或者class成员变量替换template参数。
- 因type parameters造成的代码膨胀往往可以降低,做法是让带有相同二进制表述的具现类型共享实现码。