C++箴言:从模板中疏散出参数无关的代码
副标题#e#
templates(模板)是节减时间和制止代码反复的极好要领。不必再输入20个相似的 classes,每一个包括 15 个 member functions(成员函数),你可以输入一个 class template(类模板),并让编译器实例化出你需要的 20 个 specific classes(特定类)和 300 个函数。(class template(类模板)的 member functions(成员函数)只有被利用时才会被隐式实例化,所以只有在每一个函数都被实际利用时,你才会获得全部 300 个member functions(成员函数)。)function templates(函数模板)也有相似的魅力。不必再写许多函数,你可以写一个 function templates(函数模板)并让编译器做其余的事。这不是很重要的技能吗?
是的,不错……有时。假如你不小心,利用 templates(模板)大概导致 code bloat(代码膨胀):反复的(或险些反复的)的代码,数据,或两者都有的二进制码。功效会使源代码看上去紧凑而整洁,可是方针代码臃肿而松散。臃肿而松散很少会成为时尚,所以你需要相识如何制止这样的二进制扩张。
你的主要东西有一个有气势的名字 commonality and variability analysis(通用性与可变性阐明),可是关于这个想法并没有什么有气势的对象。纵然在你的职业生涯中从来没有利用过模板,你也应该从始至终做这样的阐明。
当你写一个函数,并且你意识到这个函数的实现的某些部门和另一个函数的实现本质上是沟通的,你会仅仅复制代码吗?虽然不。你从这两个函数中疏散出通用的代码,放到第三个函数中,并让那两个函数来挪用这个新的函数。也就是说,你阐明那两个函数以找出那些通用和变革的构件,你把通用的构件移入一个新的函数,并把变革的构件保存在原函数中。雷同地,假如你写一个 class,并且你意识到这个 class 的某些构件和另一个 class 的构件是沟通的,你不要复制那些通用构件。作为替代,你把通用构件移入一个新的 class 中,然后你利用 inheritance(担任)或 composition(复合)使得本来的 classes 可以会见这些通用特性。本来的 classes 中差异的构件——变革的构件——仍保存在它们本来的位置。
#p#副标题#e#
在写 templates(模板)时,你要做同样的阐明,并且用同样的要领制止反复,但这里有一个能力。在 non-template code(非模板代码)中,反复是显式的:你可以看到两个函数或两个类之间存在反复。在 template code(模板代码)中。反复是隐式的:仅有一份 template(模板)源代码的拷贝,所以你必需造就本身去判定在一个 template(模板)被实例化多次后大概产生的反复。
譬喻,假设你要为牢靠巨细的 square matrices(正方矩阵)写一个 templates(模板),个中,要支持 matrix inversion(矩阵转置)。
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(范例参数)更不通用,可是它们是完全正当的,并且,就像在本例中,它们可以很是自然。
此刻思量以下代码:
SquareMatrix<double, 5> sm1;
...
sm1.invert(); // call SquareMatrix<double, 5>::invert
SquareMatrix<double, 10> sm2;
...
sm2.invert(); // call SquareMatrix<double, 10>::invert
这里将有两个 invert 的拷贝被实例化。这两个函数不是沟通的,因为一个浸染于 5 x 5 矩阵,而另一个浸染于 10 x 10 矩阵,可是除了常数 5 和 10 以外,这两个函数是沟通的。这是一个产生 template-induced code bloat(模板导致的代码膨胀)的经典要领。
假如你看到两个函数除了一个版本利用了 5 而另一个利用了 10 之外,对应字符全部相等,你该怎么做呢?你的直觉让你建设一个取得一个值作为一个参数的函数版本,然后用 5 或 10 挪用这个参数化的函数以取代复制代码。你的直觉为你提供了很好的要领!以下是一个劈头过关的 SquareMatrix 的做法:
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
#p#分页标题#e#
就像你能看到的,invert 的参数化版本是在一个 base class(基类)SquareMatrixBase 中的。与 SquareMatrix 一样,SquareMatrixBase 是一个 template(模板),但与 SquareMatrix 纷歧样的是,它参数化的仅仅是矩阵中的工具的范例,而没有矩阵的巨细。因此,所有持有一个给定工具范例的矩阵将共享一个单一的 SquareMatrixBase class。从而,它们共享 invert 在谁人 class 中的版本的单一拷贝。
SquareMatrixBase::invert 仅仅是一个打算用于 derived classes(派生类)以制止代码反复的要领,所以它是 protected 的而不是 public 的。挪用它的特别本钱应该为零,因为 derived classes(派生类)的 inverts 利用 inline functions(内联函数)挪用 base class(基类)的版本。(这个 inline 是隐式的——拜见《领略inline化的参与和解除》。)这些函数利用了 "this->" 标志,因为就像 Item 43 表明的,假如不这样,在 templatized base classes(模板化基类)中的函数名(诸如 SquareMatrixBase<T>)被 derived classes(派生类)埋没。还要留意 SquareMatrix 和 SquareMatrixBase 之间的担任干系是 private 的。这精确地反应了 base class(基类)存在的来由仅仅是简化 derived classes(派生类)的实现的事实,而不是暗示 SquareMatrix 和 SquareMatrixBase 之间的一个观念上的 is-a 干系。(关于 private inheritance(私有担任)的信息,拜见 《审慎利用私有担任》。)
迄今为止,还不错,可是有一个棘手的问题我们还没有提及。SquareMatrixBase::invert 奈何知道应操纵什么数据?它从它的参数知道矩阵的巨细,可是它奈何知道一个特定矩阵的数据在那边呢?或许只有 derived class(派生类)才知道这些。derived class(派生类)如何把这些转达给 base class(基类)以便于 base class(基类)可以或许做这个转置呢?
一种大概是为 SquareMatrixBase::invert 增加另一个的参数,也许是一个指向存储矩阵数据的内存块的开始位置的指针。这样可以事情,可是十有八九,invert 不是 SquareMatrix 中仅有的能被写成一种 size-independent(巨细无关)的方法并移入 SquareMatrixBase 的函数。假如有几个这样的函数,全都需要一种找到持有矩阵内的值的内存的要领。我们可觉得它们全都增加一个特另外参数,可是我们一再反复地汇报 SquareMatrixBase 同样的信息。这看上去不太正常。
一个可替换方案是让 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> 的函数。很好,不是吗?
#p#分页标题#e#
很好,是的,但不是免费的。将矩阵巨细硬性牢靠在个中的 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 指针导致封装性的低落。它也大概导致资源打点巨大化:假如 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 这样的模板就是这样做的。假如你体贴起因于你的模板的代码膨胀,你大概需要用同样的做法开拓模板。
Things to Remember
·templates(模板)发生多个 classes 和多个 functions,所以一些不依赖于 template parameter(模板参数)的模板代码会引起膨胀。
·non-type template parameters(非范例模板参数)引起的膨胀经常可以通过用 function parameters(函数参数)或 class data members(类数据成员)替换 template parameters(模板参数)而消除。
·type parameters(范例参数)引起的膨胀可以通过让具有沟通的二进制暗示的实例化范例共享实现而淘汰