条款41: 了解隐式接口和编译期多态
本条款主要是两个概念,即什么是隐式接口,什么是编译期多态。面向对象编程世界总是以显式接口(explicit interfaces)和运行期多态(runtimepolymorphism)解决问题。例如:
class Widget {
public:
Widget();
virtual ~Widget();
virtual std::size_t size()const;
virtual void normalize();
void swap(Widget& other);// see Item 25
...
};
void doProcessing(Widget& w)
{
if (w.size() > 10&& w != someNastyWidget) {
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
在Template及泛型编程的世界,显式接口和运行期多态的重要性降低。反倒是,隐式接口(implicitinterface)和编译期多态(compile-time polymorphism)更重要。
template<typename T>
void doProcessing (T& w)
{
if(w.size()>10&&w!=someNastyWidget)
{
T temp(w);
temp.normalize();
temp.swap(w);
}
}
1) 函数内w支持哪种接口是由template中执行在w身上的操作来决定。这一组接口便是T必须支持这一组隐式接口。
2) 凡涉及w的任何调用有可能造成template具现化(instantiated),使得调用得意成功,这样的具现行为发生在编译期。“以不同的template参数具现化functiontemplate”会导致调用不同的函数,这便是所谓的编译期多态。
编译期多态和运行期多态可以简单的理解成,一个是决定哪个重载函数该被调用(一般依据编译器的函数重命名规则来实现,发生在编译期),另一个是哪一个虚函数该被绑定(通过虚函数表和虚函数指针实现,发生在运行期,根据类中的虚函数指针来动态确定)。
显式接口就是一个完整的函数签名,明确指明了函数名称,参数类型,返回值。隐式接口由一系列有效表达式组成。比如这样一个表达式:
w.size()>10&&w!=someNastWidge
通过表达式来指明类型参数T具有哪些能力,这些便是隐式接口。
请记住:
1. class和template都支持接口与多态;
2. 对classes而言,接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期;
3. 对template参数而言,接口是隐式的,奠基于有效表达式。多态则是通过template具现化和函数重载解析发生于编译期。
条款42:了解typename的双重意义
Typename的第一重含义就是作为声明template类型参数,此时template<class T>和template <typename T>两者的意义完全相同。但是有时候一定要使用typename,即任何时候想要指涉一个嵌套从属类型名称,就必须紧临它前一个位置上放上关键字typename。
class Sample
{
public:
typedef int myType;
// static constint myType = 0;
};
int main()
{
Sample::myType *pb = new int(0); // 有的编译器会报错
}
有的编译器在这段代码上会产生歧义,因为它会默认的认为myType是一个静态成员,这样就变成了静态对象乘以pb。为了解决这个二义性,在前面加上typename,会强制编译器视为一个类型,而不是静态变量。但是使用typename也有两个特例,第一个是继承的时候:
class Derived:public Base::NestedClass{}; // 正确
class Derivedpublic typename Base::NextedClass(){}; // 错误
在继承某个类时,即使这个类名是嵌套从属类型也不需要使用typename,因为编译器不会认为它是个静态成员,因为继承类必然是个类型。
第二个是构造函数中的初始化成员列表:
Derived (int x):Base::NestedClass(x){}// 正确
Derived (int x):typename Base::NestedClass(x){} // 错误
此时编译器也不会将它视为静态对象,因为静态对象不支持初始化成员列表这种方式。
请记住:
1. 声明template参数时,前缀关键字class与typename可以互换;
2. 请使用关键字typename标识嵌套从属类型名称;但不得在base class lists或者member initialization list内使用typename;
条款43: 学习处理模板化基类内的名称
本条款其实就说了一个事:在继承模板化基类时,如果要使用基类中的成员,请通过”this->”或“基类资格修饰符”来完成,否则编译无法通过,因为C++具有“不进入模板化基类(templatized base classes)观察”的行为。
假设现在编写一个发送信息到不同公司的程序,信息要么译成密码,要么就是原始文字,在编译期间来决定哪一家公司发送至哪一家公司,采用template手法:
classCompanyA{
public:
void sendCleartext(conststd::string& msg);
void sendEncryted(conststd::string& msg);
……
};
class CompanyB{
public:
void sendCleartext(conststd::string& msg);
void sendEncryted(conststd::string& msg);
……
};
class MsgInfo{……};//用来保存信息,以备将来产生信息
template<typename Company>
class MsgSender{
public:
……//构造析构等函数
void sendClear(const MsgInfo& info)
{
std::string msg;
//根据info产生信息
Company c;
c.sendCleartext(msg);
}
void sendSecart(const MsgInfo& info)
{……}
};
上面这个做法行得通,但是如果要在每次送出信息时记录日志,可以派生出derived class:
template<typename Company>
class LoggingMsgSender: publicMsgSender<Company>
{
public:
……//析构构造等
void SendClearMsg(const MsgInfo& info)
{
//发送前的信息写到log
sendClear(info);//调用base class函数,这段代码无法通过编译
//传送后信息写到log
}
};
在派生类中,sendClearMsg和base class中的sendClear不同,这是个好设计,避免遮掩继承而得的名称,也避免了重新定义一个继承而得non-virtual函数。但是编译不能通过,因为编译器无法找到sendClear函数。因为当编译器遇到classtemplate LoggingMsgSender定义式时,不知道它继承什么样的class,因为MsgSender中的Company是个参数,在LoggingMsgSender被具体化之前,无法确切知道它是什么,自然而然就不知道classMsgSender是什么,也就不知道它是否有个sendClear函数。
我们必须有某种办法令C++“不进入templatized base class观察”的行为失效。有以下三种:
第一种:在base class函数调用动作之前加上“this->”
template<typename Company>
class LoggingMsgSender: publicMsgSender<Company>
{
public:
……//析构构造等
void SendClearMsg(const MsgInfo& info)
{
//发送前的信息写到log
this->sendClear(info);
//传送后信息写到log
}
};
第二种:使用using声明式
template<typename Company>
class LoggingMsgSender: publicMsgSender<Company>
{
public:
……//析构构造等
uinsg MsgSender<Company>::sendClear;//告诉编译器,假设sendClear位于base class内
void SendClearMsg(const MsgInfo& info)
{
//发送前的信息写到log
sendClear(info);//可以编译通过,假设sendClear将被继承
//传送后信息写到log
}
};
第三种:明白指出被调用的函数位于base class内(使用了明确资格修饰符(explicitqualification),这将会关闭virtual绑定行为)
template<typename Company>
class LoggingMsgSender: publicMsgSender<Company>
{
public:
……//析构构造等
void SendClearMsg(const MsgInfo& info)
{
//发送前的信息写到log
MsgSender<Company>::sendClear(info);//可以编译通过,假设sendClear被继承
//传送后信息写到log
}
};
上面三种做法原理相同:对编译器承诺“base class template的任何特化版本都将支持其一般(泛化)版本所提供的接口”。
请记住:
1. 可在derived class templates内通过this->指涉base class templates内的成员名称,或藉由一个明白写出base class资格修饰符完成。
条款44:将与参数无关的代码抽离templates
emplate是节省时间和避免重复代码的一个奇妙方法,class template的成员函数只有在被使用时才被具现化。如果你不小心,使用templates可能导致代码膨胀(codebloat):其二进制代码带着重复(或几乎重复)的代码、数据、或两者。其结果可能源码看起来合身整齐,但目标码却不是那么回事。你需要知道如何避免这样的二进制浮夸。主要工具是:共性与变性分析。在non-template中,重复十分明确,然而在template中,重复是隐晦的:你必须训练自己去感受当template被具现化多次时可能发生的重复。举个例子,矩阵template,支持矩阵inversion运算:
template<typenameT, std::size_t n>
classSquareMatrix{
public:
void invert();
};
template接受一个类型参数T,还有一个类型为size_t的非类型参数(non-typeparameter)。
SquareMatrix<double,5> sm1;
sm1.invert();
SquareMatrix<double,10> sm2;
sm2.invert();
这会具现两份invert。这些函数并非完全相同,但除了常量5和10,其他部分都相同,这是template引出代码膨胀的一个典型例子。下面是对SquareMatrix的第一次修改:
template<typenameT> //与尺寸无关的base class
classSquareMatrixBase{
protected:
void invert(std::size_t matrixSize); //以给定的尺寸求逆矩阵
};
template<typenameT, std::size_t n>
classSquareMatrix: private SquareMatrixBase<T>{
private:
using SquareMatrixBase<T>::invert;//避免遮掩base版的invert
public:
void invert() {this->invert(n);} //制造一个inline调用,用this->为了不被derived classes的函数名称掩盖
};
带参数的invert位于base class中。和SquareMatrix一样,也是个template,不同的是他只对“矩阵元素对象的类型”参数化,不对矩阵的尺寸参数化。因此对于给定的元素对象类型,所有矩阵共享同一个(也是唯一一个)SquareMatrixBaseclass。注意,SquareMatrixBase和SquareMatrix之间继承关系是private,这说明base class是为了帮助derived classes实现,两者不是is-a关系。
请记住:
1. Template生成多个classes与多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生相依关系。
2. 因非类型模板参数而造成的代码膨胀,往往可以消除,做法是以函数参数或者class成员变量替换template参数。
3. 因类型而造成的代码膨胀,也可以降低,做法是让带有完全相同二进制表述的具现类型共享实现码。
条款45:运用成员函数模板接受所有兼容类型
真实指针做得很好的一件事是,支持隐式转换。派生类指针可以隐式转换为基类指针,“指向non-const对象”的指针可以转换为“指向const对象”......等等。下面是可能发生于三层继承体系的一些转换:
class Top{...};
classMiddle:public Top{...};
classBottom:public Middle{...};
Top* pt1 = newMiddle; //将Middle*转换为Top*
Top* pt2 = newBottom; //将Bottom*转换为Top*
const Top* pct2= pt1; //将Top*转换为constTop*
但如果想在用户自定义的智能指针中模拟上述转换,稍微有点麻烦。我们希望以下代码通过编译:
template<typenameT>
class SmartPtr
{
public:
explicit SmartPtr(T* realPtr); //智能指针通常以内置(原始)指针完成初始化
...
};
SmartPt<Top>pt1 = SmartPtr<Middle>(new Middle);//将SmartPtr<Middle>转换为SmartPtr<Top>
SmartPt<Top>pt1 = SmartPtr<Bottom>(new Bottom);//将SmartPtr<Bottom>转换为SmartPtr<Top>
SmartPtr<constTop> pct2 = pt1; //将SmartPtr<Top>转化换为SmartPtr<const Top>
但是,同一个template的不同具现体之间并不存在什么与生俱来的固有关系(这里指的是如果以带有基类-派生类关系的B,D两类型分别具现化某个模板,产生出来的两个具现体并不带有基类-派生类关系),所以编译器视SmartPtr<Middle>和SmartPtr<Top>为完全不同的的类,它们之间没有任何关系。为了获得我们所希望的SmartPtr类之间的转换能力,我们必须将它们明确地编写出来。假设日后添加了:
class BelowBottom:public Bottom{...};
我们因此必须令SmartPtr<BelowBottom>对象得以生成SmartPtr<Top>对象,但我们当然不希望一再修改SmartPtr模板以满足此类需求。
就原理而言,此例中我们需要的构造函数数量没有止尽,因为一个模板可以被无限量具现化,以致生成无限量函数。因此,似乎我们需要的不是为SmartPtr写一个构造函数,而是为它写一个构造函数模板。这样的模板是所谓的成员函数模板,其作用是为类生成函数:
template<typename>
class SmartPtr
{
public:
template<typename U>
SmartPtr(const SmartPtr<U>&other); //成员模板,为了生成copy构造函数
...
};
以上代码的意思是,对任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T>——因为SmartPtr<T>有个构造函数接受一个SmartPtr<U>参数。这一类构造函数根据对象u创建对象t(例如根据SmartPtr<U>创建一个SmartPtr<T>),而u和v的类型是同一个模板的不同具现体,有时我们称之为泛化copy构造函数。
成员函数模板并不改变语言规则,而语言规则说,如果程序需要一个copy构造函数,你却没有声明它,编译器会为你暗自生成一个。在类内声明泛化copy构造函数(是个成员模板)并不会阻止编译器生成它们自己的copy构造函数(一个non-template)。所以如果你想控制copy的方方面面,你必须同时声明泛化copy构造函数和“正常的”copy构造函数。相同规则也使用于赋值操作。下面是tr1::shared_ptr的一份定义摘要:
template<classT>
class shared_ptr
{
public:
shared_ptr(shared_ptr const& r); //copy构造函数
template<class Y> //泛化copy构造函数
shared_ptr(shared_ptr<Y>const& r); //shared_ptr
shared_ptr& operator=(shared_ptrconst& r); //copy assignment
template<class Y> //泛化copy assignment
shared_ptr&operator=(shared_ptr<Y> const& r);
...
};
请记住:
1. 使用成员函数模板生成“可接受所有兼容类型”的函数
2. 如果声明成员模板用于“泛化copy构造”或“泛化assignment操作”,还是需要声明正常的copy构造函数和copyassignment操作符。
条款46:需要类型转时请为模板定义非成员函数
条款 24中提到,如果所有参数都需要隐式类型转换,该函数应当声明为非成员函数。条款24是以Rational和operator*为例子展开的,本文把这个观点推广到类模板和函数模板。 但是在类模板中,需要所有参数隐式转换的函数应当声明为友元并定义在类模板中。模板化:
template<typenameT>
class Rational{
public:
Rational(const T& numerator = 0, constT& denominator = 1);
const T numerator() const;
const T denominator() const;
};
template<typenameT>
constRational<T> operator*(const Rational<T>& lhs, constRational<T>& rhs){}
上述代码是条款24直接模板化的结果,看起来很完美但它是有问题的。比如我们有如下的调用:
Rational<int>oneHalf(1, 2); // OK
Rational<int>result = oneHalf * 2; // Error!
上述失败给我们的启示是,模板化的Rational内的某些东西似乎和其非模板版本不同。事实的确如此。传统的,编译器知道我们尝试调用什么函数(就是接受两个Rationals参数的那个operator*),但这里编译器不知道我们想要调用哪个函数。取而代之的是,它们试图想出什么函数被名为operator*的模板具现化出来。它们知道它们应该可以具现化某个“名为operator*并接受两个Rational<T>参数”的函数,但为完成这一具现化行动,必须先算出T是什么。问题是它们没这个能力。
以oneHalf进行推导,过程并不困难,operator*的第一个参数被声明为Rational<T>,而传递给operator*的第一个实参(oneHalf)的类型是Rational<int>,所以T一定是int。其他参数的推导则没有这么顺利。operator*的第二个参数被声明为Rational<int>,但传递给operator*的第二个实参(2)类型是int。编译器如何根据这个推算出T?你或许会期盼编译器使用Rational<int>的non-explicit构造函数将2转换为Rational<int>,进而将T推导为int。但它们不那么做,因为在模板实参推导过程中从不将隐式类型转换函数纳入考虑。绝不!这样的转换在函数调用过程中的确被使用,但在能够调用一个函数之前,首先必须知道那个函数存在。而为了知道它,必须先为相关的函数模板推导出参数类型(然后才可将适当的函数具现化出来)。然而模板实参推导过程中并不考虑采纳“通过构造函数而发生的”隐式类型转换。
只要利用一个事实,我们就可以缓和编译器在模板实参推导方面受到的挑战:模板类内的friend声明式可以指涉某个特定函数。那意味类Rational<T>可以声明operator*是它的一个friend函数。类模板并不依赖模板实参推导(后者只施行于函数模板身上),所以编译器总是能够在类Rational<T>具现化时得知T。因此,令Rational<int>类声明适当的operator*为其friend函数,可简化整个问题:
template<typenameT>
class Rational{
public:
friend const Rational operator*(constRational& lhs, const Rational& rhs);
};
template<typenameT>
constRational<T> operator*(const Rational<T>& lhs, constRational<T>& rhs){}
在Rational<T>中声明的friend没有添加模板参数T,这是一个简便写法,它完全等价于:friend const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs);
现在对operator*的混合式调用可以通过编译了,因为当对象oneHalf被声明为一个Rational<int>,类Rational<int>于是被具现化出来,而作为过程的一部分,friend函数operator*(接受Rational<int>参数)也被自动化声明出来。后者身为一个函数而非函数模板,因此编译器可在调用它时使用隐式转换函数(例如Rational的non-explicit构造函数),而这便是混合式调用之所以成功的原因。
此时编译可以通过,但是连接有问题,最简单的可行方法就是将operator*函数本体合并至其声明式内:
template<typenameT>
class Rational
{
public:
...
friend const Rational operator*(constRational& lhs,const Rational& rhs)
{
return Rational(lhs.numerator() *rhs.numerator(),lhs.denominator() * rhs.denominator());
}
};
这样混合模式的调用result = oneHalf * 2终于可以编译、链接并且运行了:
· 为了对所有参数支持隐式类型转换,operator*需要声明为非成员函数;
· 为了让编译器推导出模板参数,operator*需要在类中声明;
· 在类中声明非成员函数的唯一办法便是声明为friend;
· 声明的函数的同时我们有义务给出函数定义,所以在函数定义也应当放在friend声明中。
如果operator*函数体变得很大,那么inline函数就不再合适了,这时我们可以让operator*调用外部的一个辅助函数:
template<typenameT> class Rational;
template<typenameT>
const Rational<T> doMultiply(constRational<T>& lhs, const Rational<T>& rhs);
template<typename T>
class Rational{
public:
friendRational<T> operator*(const Rational<T>& lhs, constRational<T>& rhs){
returndoMultiply(lhs, rhs);
}
};
请记住:
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。
条款47:请使用traits classes表现类型信息
stl主要由“用以表现容器、迭代器和算法”的template构成,但也覆盖若干工具性的templates,其中一个名为advance,将某个迭代器移动某个给定距离:
template<typenameIterT, typename DistT>
voidadvance(IterT& iter, DistT d); //将迭代器向前移动d个单位,d<0则向后移动。
STL里面的迭代器的分五类:
· Input迭代器只能向前移动,一次一步,只读,且只能读取一次。模仿指向输入文件的阅读指针:istream_iterator是代表
· Output迭代器类似,但只可涂写它们所指的东西,ostream_iterator是代表。它们只适合“一次性操作算法”one-passalgorithm。
· forward迭代器,可以做前述两种分类所能做的每一件事,而且可以读或写其所指物一次以上。可施行于多次性操作算法。stl未提供单向linked_list,指入这种容器的迭代器就是属于forward迭代器。
· bedirectional迭代器比上一个威力更大:除了可以向前移动,还可以向后移动。stl的list、set、multiset、map和multimap的迭代器都属于这一类。
· random access迭代器比上一个更威力大,它可以执行迭代器算术,常量时间内可以向前或向后跳跃任意距离。内置指针也可以当random迭代器用。vector、deque、string提供的迭代器都是这一类。
我们真正希望的是以这种方式实现advance(伪代码如下):
template<typenameIterT, typename DistT>
voidadvance(IterT& iter, DistT d) {
if (iter is a random_access_iterator)iter += d; //针对random_access迭代器使用迭代器算术运算
else {
if (d >= 0) {
while (d--) ++iter;
}else {
while (d++) --iter;
}
}
}
这种做法必须先判断iter是否为random_access迭代器。需要取得类型的某些信息。那就是traits让你进行的事:它们允许在编译期间取得某些类型信息。traits并不是c++关键字或一个预先定义好的构件:它们是一种技术,也是一个c++程序员共同遵守的协议。这个技术的要求之一是,它对内置类型和用户自定义类型的表现必须一样好。
标准模板库是把traits信息放到模板中,其中针对迭代器的被命名为iterator_traits,它是一个结构体:
template<typenameIterT>
structiterator_traits;
它的工作原理是:针对每一个类型IterT,在结构体中声明某个typedef名为iterator_category,这个typedef用于确认IterT的迭代器分类。它包括两部分实现:
· 对于自定义类型,用户必须在类模板中声明一个typedef名为iterator_category,比如对双端队列和列表的模板类:
template<typename ...>
class deque{
public:
iterator{
public:
typedefrandom_access_iterator_tag iterator_category; //双端队列的迭代器可以随机访问
...
};
template<typename ...>
class list{
public:
iterator{
public:
typedefbidirectional_access_iterator_tag iterator_category; //列表的迭代器可以随机访问
...
};
在iterator_traits中,获取迭代器的类型信息:
template<typename IterT>
struct iterator_traits{
typedeftypename IterT::iterator_category iterator_category;
...
};
对于内置类型的指针而言,iterator_traits提供一个偏特化版本:
template<typename IterT>
struct iterator_traits<IterT*>{
typedefrandom_access_iterator_tag iterator_category; //内置类型的指针类型与random access迭代器类似
...
};
到此为止,你应该知道了如何设计和实现一个 traits class:
· 识别你想让它可用的关于类型的一些信息(例如,对于iterators(迭代器)来说,就是它们的 iterator category(迭代器种类))。
· 选择一个名字标识这个信息(例如,iterator_category)。
· 提供一个 template(模板)和一系列specializations(特化)(例如,iterator_traits),它们包含你要支持的类型的信息。
iterator_traits ——实际上是 std::iterator_traits,因为它是C++ 标准库的一部分——我们就可以改善我们的 advance 伪代码:
template<typenameIterT, typename DistT>
voidadvance(IterT& iter, DistT d)
{
if (typeid(typenamestd::iterator_traits<IterT>::iterator_category) ==
typeid(std::random_access_iterator_tag))
...
}
这个虽然看起来有点希望,但它不是我们想要的。在某种状态下,它会导致编译问题,但是我们到条款 48 再来研究它,现在,有一个更基础的问题要讨论。IterT的类型在编译期间是已知的,所以iterator_traits<IterT>::iterator_category可以在编译期间被确定。但是if 语句还是要到运行时才能被求值。为什么要到运行时才做我们在编译期间就能做的事情呢?它浪费了时间(严格意义上的),而且使我们的执行码膨胀。然而我们真正想要的是一个条件式(也就是一个if...else语句)判断“编译期核定成功”之类型。恰巧C++有一个取得这种行为的办法,那就是重载。所以为了让advance的行为如我们所期望,我们需要做的是产生两版重载函数,内含advance的本质内容,但各自接受不同类型的iterator_category对象:
template<typename IterT, typename DistT>
voiddoAdvance(IterT& iter, DistT d, random_access_iterator_tag) {
iter += d;
}
template<typename IterT, typename DistT>
voiddoAdcance(IterT& iter, DistT d, bidirectionl_iterator_tag) {
if(d >= 0) {
while (d--) ++iter;
}else {
while (d++) --iter;
}
}
template<typename IterT, typename DistT>
voiddoAdvance(IterT& iter, DistT d, input_iterator_tag) {
if (d < 0) {
throw out_of_range("Negativedistance");
}
while (d--) ++iter;
}
有了上面定义的这些重载的函数,那么advance需要做的只是调用它们并额外传递一个对象:
template<typenameIterT, typename DistT>
voidadvance(IterT& iter, DistT d)
{
doAdvance(
iter, d,
typename
std::iterator_traits<IterT>::iterator_category()
);
}
现在能够概述如何使用一个 traits class 了:
· 创建一套重载的 "worker"functions(函数)或者 function templates(函数模板)(例如,doAdvance),它们在一个traits parameter(形参)上不同。与传递的 traits 信息一致地实现每一个函数。
· 创建一个 "master"function(函数)或者 function templates(函数模板)(例如,advance)调用这些workers,传递通过一个 traits class 提供的信息。
请记住:
1. traits classes 使关于类型的信息在编译期间可用。它们使用 templates(模板)和 template specializations(模板特化)实现。
2. 结合 overloading(重载),traits classes 使得执行编译期类型 if...else 检验成为可能。
条款48:认识template元编程
Template metaprogramming(TMPS,模板元编程)是编写template-basedC++程序并执行编译期的过程。它本质上是以C++写成、执行于C++编译器内的程序。一旦TMP程序结束执行,其输出,也就是从templates具现出来的若干C++源码,便会一如往常地被编译。TMP有两个伟大的效力:第一,它让某些事情更容易。如果没有它,那些事情将是困难,甚至不可能。第二,欲与TMP执行于C++编译期,因此可将工作从运行期转移到编译期。这导致一个结果是,某些错误原本通常在运行期才能检测到,现在可在编译期找出。这样带来的结果是很美好的。因为使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存要求。然而它也会带来另一个令人感到不愉快的结果是,编译时间变长了。
为了再次认识下事物在TMP中如何运作,来看下循环。TMP没有真正循环,循环由递归(recursion)完成。TMP递归甚至不是正常的递归,因为TMP递归不涉及递归函数调用,而是涉及递归模板化(recursivetemplate instantiation)。TMP的起手程序是在编译期计算阶乘。TMP的阶乘运输示范如何通过递归模板具体化实现循环,以及如何在TMP中创建和使用变量:
template<unsignedn>
struct Factorial{
enum{value=n*Factorial<n-1>::value};
};
template<>
struct Factorial<0>{ //特殊情况,Factorial<0>的值是1
enum {value=1};
};
有了这个template metaprogram,只要指涉Factorial::value就可以得到n阶乘值。循环发生在template具体化Factorial内部指涉另一个template具体化Factorial之时。特殊情况的template特化版本Factorial<0>是递归的结束。
用Factorial示范TMP就像用hello world示范编程语言一样。为了领悟TMP之所以值得学习,就要先对它能够达成什么目标有一个比较好的理解。下面举三个例子:
- 确保量度单位正确。使用TMP就可以确保在编译期所有量度单位的组合都正确。
- 优化矩阵运算。
- 可以生成客户定制之设计模式(custom design pattern)实现品。使用policy-based design之TMP-based技术,有可能产生一些templates用来表述独立的设计项(所谓policies),然后可以任意结合它们,导致模式实现品带着客户定制的行为。
请记住:
1. Template metaprogramming(TMP,模板元编程)可将工作由运行期移到编译期,因而得以实现早期错误侦测和更高的执行效率。
2. TMP可被用来生成“基于政策选择组合”(based on combinations of policy choices)的客户定制代码,也可用来避免生成对某些特殊类型并不适合的代码。