Effective C++: 07模板与泛型编程
C++ template机制自身是一部完整的图灵机(Turing-complete):它可以被用来计算任何可计算的值。于是导出了模板元编程(TMP, template metaprogramming),创造出“在C++编译器内执行并于编译完成时停止执行”的程序。
41:了解隐式接口和编译期多态
所谓显式接口(explicit interface),是指在源码中明确可见的接口,显式接口由函数的签名式(也就是函数名称、参数类型、返回类型)构成。而运行时多态,就是指继承和virtual带来的动态绑定机制。
在Templates及泛型编程的世界里,隐式接口和编译期多态更重要一些。比如下面的模板定义:
template<typename T> void doProcessing(T& w) { if (w.size() > 10 && w != someNastyWidget) { T temp(w); temp.normalize(); temp.swap(w); } }
w必须支持哪一种接口,是由template中执行于w身上的操作来决定。本例看来w的类型T好像必须支持size,normalize和swap成员函数、copy构造函数、不等比较。这一组表达式便是T必须支持的一组隐式接口(implicit interface)。
凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template具现化(instantiated),使这些调用得以成功。这样的具现行为发生在编译期。“以不同的template参数具现化function templates”会导致调用不同的函数,这便是所谓的编译期多态。
42:了解typename的双重意义
以下template声明式中,class和typename的意义是完全相同的,但更推荐typename:
template<class T> class Widget; // uses "class" template<typename T> class Widget; // uses "typename"
但是,某种情况下必须使用typename。首先看下面的函数:
template<typename C> void print2nd(const C& container) { // this is not valid C++! if (container.size() >= 2) { C::const_iterator iter(container.begin()); // get iterator to 1st element ++iter; // move iter to 2nd element int value = *iter; // copy that element to an int std::cout << value; // print the int } }
在上面的函数模板中,iter的类型是C::const_iterator,而实际是什么必须取决于template参数C。如果 template内出现的名称相依于某个template参数,称之为从属名称(dependent names)。如果从属名称在class内呈嵌套状,我们称它为嵌套从属名称(nested dependent name )。C::const_iterator就是这样一个名称。实际上它还是个嵌套从属类型名称(nested dendent type name ),也就是个嵌套从属名称并且指涉某类型。
另一个local变量value,其类型是int。int是一个并不倚赖任何template参数的名称。这样的名称是谓非从属名称(non-dependent names)。
嵌套从属名称有可能导致解析困难。比如:
template<typename C> void print2nd(const C& container) { C::const_iterator * x; ... }
看上去好像是我们将 x 声明为一个指向 C::const_iterator 的局部变量。但编译器不这么认为,比如如果 C 有一个静态数据成员碰巧就叫做 const_iterator 呢?而且 x 碰巧是一个全局变量的名字呢?在这种情况下,上面的代码就不是声明一个 局部变量,而成为 C::const_iterator 乘以 x!
直到 C 成为已知之前,没有任何办法知道 C::const_iterator 到底是不是一个类型。C++ 有一条规则解决这个歧义:如果解析器在一个模板中遇到一个嵌套从属名字,它假定那个名字不是一个类型,除非你明确告诉它。所以,一开始的print2nd中的语句并不合法:
C::const_iterator iter(container.begin());
iter 的 声明仅在 C::const_iterator 是一个类型时才有意义,但是我们没有告诉 C++ 它是,所以C++ 就假定它不是。要想纠正这个错误,必须明确指出C::const_iterator 是一个类型:这就必须使用typename:
template<typename C> // this is valid C++ void print2nd(const C& container) { if (container.size() >= 2) { typename C::const_iterator iter(container.begin()); ... } }
一般性规则很简单:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧临它的前一个位置放上关键字typename。
typename只被用来验明嵌套从属类型名称,其他名称不该有它存在。例如:
template<typename C> // typename allowed (as is "class") void f(const C& container, // typename not allowed typename C::iterator iter); // typename required
上述的C并不是嵌套从属类型名称(它并非嵌套于任何“取决于template参数”的东西内),所以声明container时并不需要以typename为前导,但C::iterator是个嵌套从属类型名称,所以必须以typename为前导。
上面的规则有个例外:typename不可以出现在base classes list内的嵌套从属类型名称之前,也不可在成员初始化列表中作为base class修饰符。例如:
template<typename T> class Derived: public Base<T>::Nested { // base class list: typename not allowed public: explicit Derived(int x) : Base<T>::Nested(x) // base class identifier in mem { // init. list: typename not allowed typename Base<T>::Nested temp; // use of nested dependent type ... // name not in a base class list or } // as a base class identifier in a ... // mem. init. list: typename required };
来看最后一个例子:
template<typename IterT> void workWithIterator(IterT iter) { typename std::iterator_traits<IterT>::value_type temp(*iter); ... }
上面的函数以迭代器为参数,函数第一条语句的意思是为该迭代器所指向的对象创建一个副本。std::iterator_traits<IterT>::value_type 就是表示IterT所指对象的类型。比如如果 IterT 是 vector<int>::iterator,则temp 就是 int 类型。
std::iterator_traits<IterT>::value_type是一个嵌套从属类型名称(value_type 嵌套在 iterator_traits<IterT>内部,而且 IterT 是一个 模板参数),所以必须在它之前放置 typename。
43:学习处理模板化基类内的名称
看一下下面的代码,它表示需要将信息发到若干不同的公司,信息要么是明文,要么是密文:
class CompanyA { public: ... void sendCleartext(const std::string& msg); void sendEncrypted(const std::string& msg); ... }; class CompanyB { public: ... void sendCleartext(const std::string& msg); void sendEncrypted(const std::string& msg); ... }; ... // classes for other companies class MsgInfo { ... }; // class for holding information // used to create a message template<typename Company> class MsgSender { public: ... // ctors, dtor, etc. void sendClear(const MsgInfo& info) { std::string msg; create msg from info; Company c; c.sendCleartext(msg); } void sendSecret(const MsgInfo& info) // similar to sendClear, except { ... } // calls c.sendEncrypted };
现在假设有了新的需求,需要在每次发送明文是记录日志。此时可以通过继承实现:
template<typename Company> class LoggingMsgSender: public MsgSender<Company> { public: ... // ctors, dtor, etc. void sendClearMsg(const MsgInfo& info) { //write "before sending" info to the log; sendClear(info); // call base class function; // this code will not compile! //write "after sending" info to the log; } ... };
上面的代码无法通过编译,编译器会抱怨sendClear不存在。尽管在base class中确实定义了sendClear。
问题在于,当编译器见到LoggingMsgSender这个模板类的定义时,并不知道它继承什么样的类。当然它继承的是MsgSender<Company>,但其中的Company是个template参数,只有在具现化LoggingMsgSender才知道Company是什么,而如果不知道Company是什么,就无法知道MsgSender<Company>是否有个sendClear函数。
比如,现在有个公司只能发送密文:
class CompanyZ { // this class offers no public: // sendCleartext function ... void sendEncrypted(const std::string& msg); ... };
此时一般性的MsgSender对CompanyZ就不合适了,必须产生一个MsgSender特化版:
template<> // a total specialization of class MsgSender<CompanyZ> { // MsgSender; the same as the public: // general template, except ... // sendCleartext is omitted void sendSecret(const MsgInfo& info) { ... } };
现在有个MsgSender针对CompanyZ的全特化版本,再次考虑LoggingMsgSender的实现,当base class指定为MsgSender<CompanyZ>时,这段代码不合法,因为该base class没有sendClear函数。
这就是编译失败的原因,编译器知道base class模板类可能被特化,而特化版本可能不提供一般性模板相同的接口,所以它才拒绝在模板化基类(本例中的MsgSender<Company>)内寻找继承而来的名称(本例中的SendClear)。
有三种办法可以明确指出使编译器进入模板基类中寻找名称:第一是在base class的函数调用时加上this->:
template<typename Company> class LoggingMsgSender: public MsgSender<Company> { public: ... void sendClearMsg(const MsgInfo& info) { //write "before sending" info to the log; this->sendClear(info); // okay, assumes that sendClear will be inherited //write "after sending" info to the log; } ... };
第二是使用using声明式:
template<typename Company> class LoggingMsgSender: public MsgSender<Company> { public: ... using MsgSender<Company>::sendClear; // tell compilers to assume // that sendClear is in the base class void sendClearMsg(const MsgInfo& info) { ... sendClear(info); // okay, assumes that sendClear will be inherited ... } };
第三是明确指出函数位于base class内:
template<typename Company> class LoggingMsgSender: public MsgSender<Company> { public: ... void sendClearMsg(const MsgInfo& info) { ... MsgSender<Company>::sendClear(info); // okay, assumes that ... // sendClear will be inherited } };
第三种方法有个缺点,就是当被调用的virtual函数时,会关闭virtual绑定行为。
从名字可见性的观点来看,上面方法都做了同样的事情:它向编译器保证任何后继的 base class template(基类模板)的特化版本都将支持通用模板提供的接口。
但是如果保证被证实不成立,真相将在后继的编译过程中暴露。例如,如果后面的源代码中包含这些:
LoggingMsgSender<CompanyZ> zMsgSender; MsgInfo msgData; ... // put info in msgData zMsgSender.sendClearMsg(msgData); // error! won't compile
对 sendClearMsg 的调用将不能编译,因为在此刻,编译器知道 base class是MsgSender<CompanyZ>,也知道那个类没有提供 sendClear函数。
从根本上说,问题就是编译器是早些(当 derived class template definitions(派生类模板定义)被解析的时候)诊断对 base class members(基类成员)的非法引用,还是晚些时候(当那些 templates(模板)被特定的 template arguments(模板参数)实例化的时候)再进行。C++ 的方针是宁愿早诊断,而这就是为什么当那些 classes(类)被从 templates(模板)实例化的时候,它假装不知道 base classes(基类)的内容。
44:将与参数无关的代码抽离template
为了避免重复代码,当编写某个普通函数,其中某些部分的实现码和另一个函数的实现码实质相同,此时,抽出两个函数的共同部分,把它们放进第三个函数中,然后令原先两个函数调用这个新函数。同样道理,如果你正在编写某个class,而其中某些部分和另一个class的某些部分相同,可以把共同部分搬移到新class去,然后使用继承或复合,令原先的classes取用这共同特性。而原classes的互异部分仍然留在原位置不动。
编写templates时,也可以做同样的优化,以相同的方式避免重复。在non-template代码中,重复十分明确;然而在template代码中,重复是隐晦的。
举个例子,为固定尺寸的正方矩阵编写一个支持逆矩阵运算的template:
template<typename T, std::size_t n> class SquareMatrix { public: ... void invert(); }; SquareMatrix<double, 5> sm1; sm1.invert(); // call SquareMatrix<double, 5>::invert SquareMatrix<double, 10> sm2; sm2.invert(); // call SquareMatrix<double, 10>::invert
sm1.invert和sm2.invert函数调用会具现化两份invert。这些函数并非完完全全相同,但除了常量5和10,两个函数的其他部分完全相同。这是template引出代码膨胀的一个典型例子。
首先想到为它们建立一个带数值参数的函数,然后以5和10来调用这个带参数的函数,而不重复代码:
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; public: ... void invert() { this->invert(n); } };
SquareMatrixBase也是个template,不同的是它只对“矩阵元素对象的类型”参数化,不对矩阵的尺寸参数化。因此对于某给定的元素对象类型,所有矩阵共享同一个SquareMatrixBase class。它们也将因此共享这唯一一个class内的invert函数。
45:运用成员函数模板接受所有兼容类型
智能指针的行为像指针,并提供真实指针没有的机制保证资源自动回收。真实指针支持隐式转换:Derived class指针可以隐式转换为base class指针,指向non-const对象的指针可以转换为指向const对象的指针等,比如:
class Top { ... }; class Middle: public Top { ... }; class Bottom: public Middle { ... }; Top *pt1 = new Middle; // convert Middle* => Top* Top *pt2 = new Bottom; // convert Bottom* => Top* const Top *pct2 = pt1; // convert Top* => const Top*
如果想在自定义的智能指针中模拟上述转换:
template<typename T> class SmartPtr { public: // smart pointers are typically explicit SmartPtr(T *realPtr); // initialized by built-in pointers ... }; SmartPtr<Top> pt1 = // convert SmartPtr<Middle> => SmartPtr<Middle>(new Middle); // SmartPtr<Top> SmartPtr<Top> pt2 = // convert SmartPtr<Bottom> => SmartPtr<Bottom>(new Bottom); // SmartPtr<Top> SmartPtr<const Top> pct2 = pt1; // convert SmartPtr<Top> => // SmartPtr<const Top>
上面的代码是错误的。同一个template的不同具现体之间并不存在什么先天的固有关系,也就是说:如果以带有base-derived关系的S, D两类型分别具现化某个template,产生出来的两个具现体并不带有base-derived关系。
所以,为了获得SmartPtr classes之间的转换能力,必须明确写出来。上面的语句时创建智能指针对象,所以考虑构造函数的实现。但是不可能写出所有需要的构造函数,上面的代码中,根据一个SmartPtr<Middle> 或 SmartPtr<Bottom> 构造出一个 SmartPtr<Top>,但是如果将来这个继承体系被扩充,还需要重新定义一个构造函数,这是不现实的。
我们需要的不是为SmartPtr写一个构造函数,而是写一个构造模板,这就是所谓的member function templates:
template<typename T> class SmartPtr { public: template<typename U> // member template SmartPtr(const SmartPtr<U>& other); // for a "generalized ... // copy constructor" };
这个构造模板的意思是:对任何类型T和任何类型U,这里可以根据SmartPtr<U>生成一个SmartPtr<T>,有时又称之为泛化copy构造函数。
上面的声明还不够:我们希望根据一个SmartPtr<Bottom> 创建一个 SmartPtr<Top>,但是不需要能够从一个 SmartPtr<Top> 创建一个 SmartPtr<Bottom>,因此必须在某方面对这个member template所创建的成员函数群进行筛除。可以这样做:
template<typename T> class SmartPtr { public: template<typename U> SmartPtr(const SmartPtr<U>& other) // initialize this held ptr : heldPtr(other.get()) { ... } // with other's held ptr T* get() const { return heldPtr; } ... private: // built-in pointer held T *heldPtr; // by the SmartPtr };
这里使用U*初始化T*,这个行为只有在:存在某个隐式转换可将U*指针转换为T*指针时才能通过编译。
成员函数模板的作用并不仅限于构造函数,它还可以作用于赋值操作副。
如果类没有定义copy构造函数,编译器会自动生成一个。在类内声明泛化copy构造函数并不会阻止编译器生成它们自己的copy构造函数。这个规则也适用于赋值操作。
46:需要类型转换时请为模板定义非成员函数
条款24讨论过为什么只有非成员函数才能“在所有实参身上实施隐式类型转换”,该条款以Rational的operator*函数为例。现在将Rational和operator*模板化,代码如下:
template<typename T> class Rational { public: Rational(const T& numerator = 0, // see Item 20 for why params const T& denominator = 1); // are now passed by reference const T numerator() const; // see Item 28 for why return const T denominator() const; // values are still passed by value, ... // Item 3 for why they're const }; template<typename T> const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) { ... }
像条款24一样,我们希望支持混合式算术运算,所以我们希望下面的代码通过编译:
Rational<int> oneHalf(1, 2); Rational<int> result = oneHalf * 2; // error! won't compile
上面的代码并不能通过编译。在条款24中,编译器知道我们尝试调用什么函数,但是这里编译器却不知道。虽然这里有个函数模板是operator*,它接受两个Rational<T>参数,但是在模板实参推导过程中,从不考虑隐式类型转换。
这里的调用中,operator*的参数一个是Rational<int>,另一个是int类型,没有这样的模板函数可以具现化出这样的函数,所以编译失败。
可以利用以下规则解决这个问题:模板类中的friend声明可以指涉某个特定函数:
template<typename T> class Rational { public: ... friend const Rational operator*(const Rational& lhs, const Rational& rhs); }; template<typename T> // define operator* const Rational<T> operator*(const Rational<T>& lhs, // functions const Rational<T>& rhs) { ... }
此时,当对象oneHalf被声明为一个Rational<int>时,类Rational<int>也就被具现化出来了,那么friend函数operator*也就自动被声明出来。后者作为一个函数而不是函数模板,编译器可以再调用它时使用隐式转换。
此时,代码虽然能够通过编译,但是却无法链接成功。当声明一个Rational<int>时,该类被具现化出来,但其中的friend声明也仅仅是个声明,还没有找到定义,也就是函数体并未具现化(尽管在Rational外部提供了该friend的定义,但是那是一个函数模板,在没有遇到参数匹配的函数调用之前,不会具现化,这里的调用时oneHalf*2,参数不匹配,所以不会具现化这个模板)。
最简单的解决办法就是讲定义体放在Rational内:
template<typename T> class Rational { public: ... friend const Rational operator*(const Rational& lhs, const Rational& rhs) { return Rational(lhs.numerator() * rhs.numerator(), // same impl lhs.denominator() * rhs.denominator()); // as in } // Item 24 };
现在可以通过编译、连接,并能执行了。
这个技术的趣味点是:虽然使用了friend,但是与friend的传统用途“访问class中非public部分”毫不相干:为了让类型转换可以作用于所有实参上,需要一个non-member函数(条款24);为了这个函数能自动具现化,需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是令他成为一个friend。
条款30中说,class内定义的函数都自动成为inline,包括像operator*这样的friend函数。可以这样将inline声明所带来的冲击最小化:让operator*不做任何事情,而是调用一个定义与class外部的辅助函数(本例中意义不大,因为operator*已经是个单行函数,但对复杂函数而言,这样做也许有意义)。
Rational是个模板,意味着那个辅助函数通常也是个模板,所以代码如下:
template<typename T> class Rational; template<typename T> const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs); template<typename T> class Rational { public: ... friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs) // Have friend { return doMultiply(lhs, rhs); } // call helper ... }; template<typename T> // define const Rational<T> doMultiply(const Rational<T>& lhs, // helper const Rational<T>& rhs) // template in { // header file, return Rational<T>(lhs.numerator() * rhs.numerator(), // if necessary lhs.denominator() * rhs.denominator()); }
因为定义在Rational内部的operator*需要调用doMultiply函数模板,所以,需要在Rational之前声明doMultiply,而doMultiply原型中,又用到了Rational模板,所以在它之前又需要声明Rational。
作为一个template,doMultiply当然不支持混合式乘法,但它其实不需要。他只是被operator*调用,而operator*支持混合式乘法,也就是调用operator*时,参数已经完成了隐式转换。
47:使用traits classes表现类型信息
STL中有一个名为advance的template,它用于将某个迭代器移动指定距离:
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d);
只有随机访问迭代器支持+=操作,所以其他类型的迭代器,在advance中只能反复执行++或--操作,供d次。
STL共有5中迭代器分类,对应于它们支持的操作:input迭代器只能向前移动,一次一步,只能读取它们所指的东西,而且只能读取一次,istream_iterators就是这种迭代器;output迭代器类似,只能向前移动,一次一步,只能写它们所指的东西,且只能写一次,ostream_iterators是这类迭代器;forward迭代器,可以做前述两种类型迭代器所能做的每件事,且可以读或写所指物一次以上,单向链表类型的容器的迭代器就属于forward迭代器;bidirectional迭代器除了可以向前移动,也可以向后移动。set,map等的迭代器属于这一类;最强大的迭代器是random access迭代器,它更强大的地方在于可以执行迭代器算术,也就是常量时间内向前或向后跳跃任意距离。vector、deque,string的迭代器属于这一类,指针也被当做random access迭代器。
对于这5中迭代器,C++标准库提供了卷标结构用以区分:
struct input_iterator_tag {}; struct output_iterator_tag {}; struct forward_iterator_tag: public input_iterator_tag {}; struct bidirectional_iterator_tag: public forward_iterator_tag {}; struct random_access_iterator_tag: public bidirectional_iterator_tag {};
回到advance函数,它的伪代码应该是下面这个样子:
template<typename IterT, typename DistT> void advance(IterT& iter, DistT d) { if (iter is a random access iterator) { iter += d; // use iterator arithmetic } // for random access iters else { if (d >= 0) { while (d--) ++iter; } // use iterative calls to else { while (d++) --iter; } // ++ or -- for other } // iterator categories }
这就需要判断iter是否为random access迭代器,也就是我们需要取得类型信息。这就是traits的作用,它允许你在编译期获得某些类型信息。traits是一种技术,也是一种约定,它的要求之一是需要对内置类型和用户自定义类型要表现的一样好。也就是说,advance收到的实参如果是一个指针,则advance也需要能够运作。
因为traits需要支持内置类型,所以traits信息必须位于类型自身之外。比如,在标准库中,针对迭代器的traits就命名为iterator_traits:
template<typename IterT> struct iterator_traits;
习惯上,traits总是被实现为structs。iterator_traits的运作方式是,针对每一个类型IterT,在struct iterator_traits<IterT>内一定声明某个typedef名为iterator_category。这个typedef用来确认IterT的迭代器分类。
iterator_traits以两部分实现上述所言。首先它要求每一个用户自定义的迭代器类型必须嵌套一个typedef,名为iterator_category。比如deque和list的迭代器定义:
template < ... > class deque { public: class iterator { public: typedef random_access_iterator_tag iterator_category; ... }; ... }; template < ... > class list { public: class iterator { public: typedef bidirectional_iterator_tag iterator_category; ... }; ... };
而在iterator_traits这个模板类中,只是简单的鹦鹉学舌:
// the iterator_category for type IterT is whatever IterT says it is; template<typename IterT> struct iterator_traits { typedef typename IterT::iterator_category iterator_category; ... };
上面的做法对于指针是行不通的,所以有一个偏特化版本:
template<typename IterT> struct iterator_traits<IterT*> { typedef random_access_iterator_tag iterator_category; ... };
现在可以对advance实践先前的伪代码:
template<typename IterT, typename DistT> void advance(IterT& iter, DistT d) { if (typeid(typename std::iterator_traits<IterT>::iterator_category) == typeid(std::random_access_iterator_tag)) ... }
实际上这个实现是有编译问题的,而且IterT类型在编译期间就可知了,所以iterator_traits<IterT>::iterator_category也可以在编译期间确定。但if语句却是在运行期才能确定。
我们真正需要的是在编译期间就能判断类型是否相同,在C++中,函数的重载就是在编译期间确定类型的例子。所以,可以使用重载技术实现advance:
template<typename IterT, typename DistT> void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag) { iter += d; } template<typename IterT, typename DistT> void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag) { if (d >= 0) { while (d--) ++iter; } else { while (d++) --iter; } } template<typename IterT, typename DistT> void doAdvance(IterT& iter, DistT d, std::input_iterator_tag) { if (d < 0 ) { throw std::out_of_range("Negative distance"); } while (d--) ++iter; }
forward_iterator_tag 继承自 input_iterator_tag,所以上面的doAdvance的input_iterator_tag版本也能处理forward迭代器。
实现了这些doAdvance重载版本之后,advance的代码如下:
template<typename IterT, typename DistT> void advance(IterT& iter, DistT d) { doAdvance( iter, d, typename std::iterator_traits<IterT>::iterator_category() ); }
TR1导入了许多新的traits classes用以提供类型信息,包括is_fundamental<T>判断T是否为内置类型,is_array<T>判断T是否是数组,以及is_base_of<T1, T2>判断T1和T2是否相同,或者是否T1是T2的base class。
48:认识template元编程
所谓template metaprogram(TMP,模板元程序)是以C++写成、执行于C++编译期内的程序。一旦TMP程序结束执行,其输出,也就是从templates具现出来的若干C++源码,便会一如往常地被编译。
TMP有两个效力。第一,它让某些事情更容易。如果没有它,那些事情将是困难的,甚至不可能的。第二,由于template metaprograms执行于C++编译期,因此可将工作从运行期转移到编译期。这导致的一个结果是,某些错误原本通常在运行期才能侦测到,现在可在编译期找出来。另一个结果是,使用TMP的C++程序可能在每一方面都更高效:较小的可执行文件、较短的运行期、较少的内存需求。然而将工作从运行期移转至编译期的另一个结果是,编译时间变长了。
条款47中提到的advance的traits实现,就是TMP的例子。在条款47中,还提到了一种运行期判断类型的实现:
template<typename IterT, typename DistT> void advance(IterT& iter, DistT d) { if (typeid(typename std::iterator_traits<IterT>::iterator_category) == typeid(std::random_access_iterator_tag)) { iter += d; // use iterator arithmetic } // for random access iters else { if (d >= 0) { while (d--) ++iter; } // use iterative calls to else { while (d++) --iter; } // ++ or -- for other } // iterator categories }
这个实现不但效率低,而且还会存在编译问题,比如针对std::list<int>::iterator iter的具现化代码如下:
void advance(std::list<int>::iterator& iter, int d) { if (typeid(std::iterator_traits<std::list<int>::iterator>::iterator_category) == typeid(std::random_access_iterator_tag)) { iter += d; // error! } else { if (d >= 0) { while (d--) ++iter; } else { while (d++) --iter; } } }
问题出在iter+=d上,这条语句尝试在list<int>::iterator上使用其不支持的+=操作。尽管在运行期间永远不会执行+=操作,但是编译期间编译器必须确保所有源码有效,某条语句执行类型不支持某种操作肯定会引起编译错误。
TMP已被证明是个图灵完备的,也就是说TMP可以计算任何事物,使用TMP可以声明变量,执行循环,编写调用函数等。比如循环,TMP中没有真正的循环构件,循环效果是通过递归实现的。以计算阶乘为例:
template<unsigned n> struct Factorial { enum { value = n * Factorial<n-1>::value }; }; template<> // special case: the value of struct Factorial<0> { // Factorial<0> is 1 enum { value = 1 }; };
可以这样使用Factorial:
int main() { std::cout << Factorial<5>::value; // prints 120 std::cout << Factorial<10>::value; // prints 3628800 }
以上只是一个TMP最简单的例子,TMP还有很多更酷的玩法。
TMP的缺点是语法不够直观,或许TMP不会成为主流,但是对某些程序员,特别是程序库开发人员,几乎肯定会成为他们的主要粮食。