templates(模板)是节省时间和避免代码重复的极好方法。不必再输入20个相似的 classes,每一个包含 15 个 member functions(成员函数),你可以输入一个 class template(类模板),并让编译器实例化出你需要的 20 个 specific classes(特定类)和 300 个函数。 (class template(类模板)的 member functions(成员函数)只有被使用时才会被隐式实例化,所以只有在每一个函数都被实际使用时,你才会得到全部 300 个member functions(成员函数)。)function templates(函数模板)也有相似的魅力。不必再写很多函数,你可以写一个 function templates(函数模板)并让编译器做其余的事。这不是很重要的技术吗?
你的主要工具有一个有气势的名字 commonality and variability analysis(通用性与可变性分析),但是关于这个想法并没有什么有气势的东西。即使在你的职业生涯中从来没有使用过模板,你也应该从始至终做这样的分析。
当你写一个函数,而且你意识到这个函数的实现的某些部分和另一个函数的实现本质上是相同的,你会仅仅复制代码吗?当然不。你从这两个函数中分离出通用的代码,放到第三个函数中,并让那两个函数来调用这个新的函数。也就是说,你分析那两个函数以找出那些通用和变化的构件,你把通用的构件移入一个新的函数,并把变化的构件保留在原函数中。类似地,假如你写一个 class,而且你意识到这个 class 的某些构件和另一个 class 的构件是相同的,你不要复制那些通用构件。作为替代,你把通用构件移入一个新的 class 中,然后你使用 inheritance(继续)或 composition(复合)使得原来的 classes 可以访问这些通用特性。原来的 classes 中不同的构件——变化的构件——仍保留在它们原来的位置。
template<typename T, // template for n x n matrices of std::size_t n> // objects of type T; see below for info class SquareMatrix { // on the size_t parameter public: ... void invert(); // invert the matrix in place }; 这个 template(模板)取得一个 type parameter(类型参数)T,但是它还有一个类型为 size_t 的参数——一个 non-type parameter(非类型参数)。non-type parameter(非类型参数)比 type parameter(类型参数)更不通用,但是它们是完全合法的,而且,就像在本例中,它们可以非常自然。
template<typename T> // size-independent base class for class SquareMatrixBase { // square matrices PRotected: ... void invert(std::size_t matrixSize); // invert matrix of the given size ... };
template< typename T, std::size_t n> class SquareMatrix: private SquareMatrixBase<T> { private: using SquareMatrixBase<T>::invert; // avoid hiding base version of // invert; see Item 33 public: ... void invert() { this->invert(n); } // make inline call to base class }; // version of invert; see below // for why "this->" is here 就像你能看到的,invert 的参数化版本是在一个 base class(基类)SquareMatrixBase 中的。与 SquareMatrix 一样,SquareMatrixBase 是一个 template(模板),但与 SquareMatrix 不一样的是,它参数化的仅仅是矩阵中的对象的类型,而没有矩阵的大小。因此,所有持有一个给定对象类型的矩阵将共享一个单一的 SquareMatrixBase class。从而,它们共享 invert 在那个 class 中的版本的单一拷贝。
迄今为止,还不错,但是有一个棘手的问题我们还没有提及。SquareMatrixBase::invert 怎样知道应操作什么数据?它从它的参数知道矩阵的大小,但是它怎样知道一个特定矩阵的数据在哪里呢?大概只有 derived class(派生类)才知道这些。derived class(派生类)如何把这些传达给 base class(基类)以便于 base class(基类)能够做这个转置呢?
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
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 ...
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 的版本用于多种矩阵大小缩小了可执行码的大小,而且还能缩小程序的 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 指针导致封装性的降低。它也可能导致资源治理复杂化:假如 base class(基类)存储了一个指向矩阵数据的指针,但是那些数据既可以是动态分配的也可以是物理地存储于 derived class object(派生类对象)之内的(就像我们看到的),它如何决定这个指针是否应该被删除?这样的问题有答案,但是你越想让它们更加精巧一些,它就会变成更复杂的事情。 在某些条件下,少量的代码重复就像是一种解脱。
本文只讨论了由于 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 这样的模板就是这样做的。假如你关心起因于你的模板的代码膨胀,你可能需要用同样的做法开发模板。