Templates是节省时间和避免代码重复的一个奇方妙法。不再需要键入20个类似的classes而每一个带有15个成员函数,你只需键入一个class template,留给编译器去具现化那20个你需要的相关classes和300个函数。(class templates的成员函数只有在被使用时才被暗中具现化,所以只有在这300个函数的每一个都被使用,你才会获得这300个函数。)Function templates有类似的诉求。替换写许多函数,你只需要写一个function templates,然后让编译器做剩余的事情。
有时候,如果你不小心,使用templates可能会导致代码膨胀:其二进制码带着重复(或几乎重复)的代码、数据,或两者。其结果又可能源码看起来合身而整齐,但目标码(object code)却不是那么回事。
当你编写某个函数,而你明白其中某些部分的实现码和另一个函数的实现码实质相同,你会很单纯地重复这些码码?当然不。你会抽出两个函数的共同部分,把它们放进第三个函数中,然后令原先两个函数调用这个新函数。也就是说,你分析了两个函数,找出共同的部分和变化的部分,把共同部分搬到一个新函数去,保留变化的部分在原函数中不动。
编写templates时,也是做相同的分析,以相同的方式避免重复,但其中有个窍门。在non-template代码中,重复十分明确:你可以“看”到两个函数或两个classes之间有所重复。然而在template代码中,重复是隐晦的:毕竟只存在一份template源码,所以你必须训练自己去感受当template被具现化多次时可能发生的重复。
举个例子,假设你想为固定尺寸的正方矩阵编写一个template。该矩阵的性质之一是支持逆矩阵运算。
template<typename T, std::size_t n>
class SquareMatrix {
public:
...
void invert(); // 求逆矩阵
};
这个template接受一个类型参数T,除此之外还接受一个类型为size_t的参数,那是个非类型参数。这种参数和类型参数比起来较不常见,但它们完全合法,而且就像本例一样,相当自然。
现在,考虑这些代码:
SquareMatrix<double,5> sml;
...
sm1.invert(); // 调用SquareMatrix<double,5>::invert
SquareMatrix<double,10> sm2;
...
sm2.invert(); // 调用SquareMatrix<double,10>::invert
这回具现化两份invert。这些函数并非完完全全相同,因为其中一个操作的是5*5矩阵而另一个操作的是10*10矩阵,但除了常量5和10,两个函数的其他部分完全相同。这是template引出代码膨胀的一个典型例子。
如果你看到两个函数完全相同,只除了一个使用5而另一个使用10,你会怎么做?你的本能会为它们建立一个带数值参数的函数,然后以5和10来调用这个带参数的函数,而不重复代码。下面是对SquareMatrix第一次修改:
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; // 避免遮掩base班的invert;见条款33
public:
...
void invert() { this->invert(n); } // 制造一个inline调用,调用base class版的invert。
};
目前为止一切都好,但还有一些棘手的问题没有解决。SquareMatrixBase::invert如何知道该操作什么数据?虽然它从参数中知道矩阵尺寸,但它如何知道哪个特定矩阵的数据在哪儿?想必只有derived class知道。我们可以令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; // 指针,指向矩阵内容
};
这允许derived classes决定内存分配方式。某些实现版本也许会决定将矩阵数据存储在SquareMatrix对象内部:
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix() // 送出矩阵大小和数据指针给base class
: SquareMatrixBase<T>(n, data) {}
...
private:
T data[n*n];
};
这种类型的对象不需要动态分配内存,但对象自身可能非常大。另一种做法是每一个矩阵的数据放进heap(也就是通过new来分配内存):
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix() // 将base class的数据指针设为null,为矩阵内容分配内存
: SquareMatrixBase<T>(n, 0),
pData(new T[n*n]) // 将指向该内存的指针存储起来,
{ this->setDataPtr(pData.get()); } // 然后将它的一个副本交给base class
...
private:
bosst::scoped_array<T> pData;
};
这个条款只讨论由non-type template parameters(非类型模板参数)带来的膨胀,其实type parameters(类型参数)也会导致膨胀。例如在许多平台上int和long有相同的二进制表述,所以像vector<int>和vector<long>的成员函数有可能完全相同——这正是最佳定义。
请记住
- Templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
- 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可以消除,做法是以函数参数或class成员变量替换template参数。
- 因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有完全相同的二进制表述的具现类型共享实现码。