一个可替换方案是让 SquareMatrixBase 存储一个指向矩阵的值的内存区域的指针。而且一旦它存储了这个指针,它同样也可以存储矩阵大小。最后得到的设计大致就像这样:
template<typename T>
class SquareMatrixBase {
protected:
SquareMatrixBase(std::size_t n, T *pMem) // store matrix size and a
: size(n), pData(pMem) {} // ptr to matrix values
void setDataPtr(T *ptr) { pData = ptr; } // reassign pData
...
private:
std::size_t size; // size of matrix
T *pData; // pointer to matrix values
};
这样就是让 derived classes(派生类)决定如何分配内存。某些实现可能决定直接在 SquareMatrix object 内部存储矩阵数据:
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix() // send matrix size and
: SquareMatrixBase<T>(n, data) {} // data ptr to base class
...
private:
T data[n*n];
};
这种类型的 objects 不需要 dynamic memory allocation(动态内存分配),但是这些 objects 本身可能会非常大。一个可选方案是将每一个矩阵的数据放到 heap(堆)上:
template<typename T, std::size_t n>
class SquareMatrix: private SquareMatrixBase<T> {
public:
SquareMatrix() // set base class data ptr to null,
: SquareMatrixBase<T>(n, 0), // allocate memory for matrix
pData(new T[n*n]) // values, save a ptr to the
{ this->setDataPtr(pData.get()); } // memory, and give a copy of it
... // to the base class
private:
boost::scoped_array<T> pData; // see Item 13 for info on
}; // boost::scoped_array
无论数据存储在哪里,从膨胀的观点来看关键的结果在于:现在 SquareMatrix 的许多——也许是全部—— member functions(成员函数)可以简单地 inline 调用它的 base class versions(基类版本),而这个版本是与其它所有持有相同数据类型的矩阵共享的,而无论它们的大小。与此同时,不同大小的 SquareMatrix objects 是截然不同的类型,所以,例如,即使 SquareMatrix<double, 5> 和 SquareMatrix<double, 10> objects 使用 SquareMatrixBase<double> 中同样的 member functions(成员函数),也没有机会将一个 SquareMatrix<double, 5> object 传送给一个期望一个 SquareMatrix<double, 10> 的函数。很好,不是吗?
很好,是的,但不是免费的。将矩阵大小硬性固定在其中的 invert 版本很可能比将大小作为一个函数参数传入或存储在 object 中的共享版本能产生更好的代码。例如,在 size-specific(特定大小)的版本中,sizes(大小)将成为 compile-time constants(编译期常数),因此适用于像 constant propagation 这样的优化,包括将它们作为 immediate operands(立即操作数)嵌入到生成的指令中。在 size-independent version(大小无关版本)中这是不可能做到的。
另一方面,将唯一的 invert 的版本用于多种矩阵大小缩小了可执行码的大小,而且还能缩小程序的 working set(工作区)大小以及改善 instruction cache(指令缓存)中的 locality of reference(引用的局部性)。这些能使程序运行得更快,超额偿还了失去的针对 invert 的 size-specific versions(特定大小版本)的任何优化。哪一个效果更划算?唯一的分辨方法就是在你的特定平台和典型数据集上试验两种方法并观察其行为。
另一个效率考虑关系到 objects 的大小。如果你不小心,将函数的 size-independent 版本(大小无关版本)上移到一个 base class(基类)中会增加每一个 object 的整体大小。例如,在我刚才展示的代码中,即使每一个 derived class(派生类)都已经有了一个取得数据的方法,每一个 SquareMatrix object 都还有一个指向它的数据的指针存在于 SquareMatrixBase class 中,这为每一个 SquareMatrix object 至少增加了一个指针的大小。通过改变设计使这些指针不再必需是有可能的,但是,这又是一桩交易。例如,让 base class(基类)存储一个指向矩阵数据的 protected 指针导致在 Item 22 中描述的封装性的降低。它也可能导致资源管理复杂化:如果 base class(基类)存储了一个指向矩阵数据的指针,但是那些数据既可以是动态分配的也可以是物理地存储于 derived class object(派生类对象)之内的(就像我们看到的),它如何决定这个指针是否应该被删除?这样的问题有答案,但是你越想让它们更加精巧一些,它就会变成更复杂的事情。在某些条件下,少量的代码重复就像是一种解脱。
本 Item 只讨论了由于 non-type template parameters(非类型模板参数)引起的膨胀,但是 type parameters(类型参数)也能导致膨胀。例如,在很多平台上,int 和 long 有相同的二进制表示,所以,可以说,vector<int> 和 vector<long> 的 member functions(成员函数)很可能是相同的——膨胀的恰到好处的解释。某些连接程序会合并同样的函数实现,还有一些不会,而这就意味着在一些环境上一些模板在 int 和 long 上都被实例化而能够引起代码重复。类似地,在大多数平台上,所有的指针类型有相同的二进制表示,所以持有指针类型的模板(例如,list<int*>,list<const int*>,list<SquareMatrix<long, 3>*> 等)应该通常可以使用每一个 member function(成员函数)的单一的底层实现。典型情况下,这意味着与 strongly typed pointers(强类型指针)(也就是 T* 指针)一起工作的 member functions(成员函数)可以通过让它们调用与 untyped pointers(无类型指针)(也就是 void* 指针)一起工作的函数来实现。一些标准 C++ 库的实现对于像 vector,deque 和 list 这样的模板就是这样做的。如果你关心起因于你的模板的代码膨胀,你可能需要用同样的做法开发模板。
Things to Remember
templates(模板)产生多个 classes 和多个 functions,所以一些不依赖于 template parameter(模板参数)的模板代码会引起膨胀。non-type template parameters(非类型模板参数)引起的膨胀常常可以通过用 function parameters(函数参数)或 class data members(类数据成员)替换 template parameters(模板参数)而消除。type parameters(类型参数)引起的膨胀可以通过让具有相同的二进制表示的实例化类型共享实现而减少。