41 了解隐式接口和编译期多态
- OOP总是通过显式接口和运行期多态解决问题,如函数doProcessing内的w
- 类型被声明为Widget,所以必须支持Widget接口,可以在源码中找到此接口(例如在Widget的.h文件中),称其为显式接口
- w对虚函数的调用表现出运行期多态
class Widget {
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
...
};
void doProcessing(Widget& w)
{
if (w.size() > 10 && w != someNasyWidget) {
Widget temp(w);
temp.normalize();
temp.swap(w);
}
}
- 在Template和泛型编程的世界中,显式接口和运行期多态仍然存在,但隐式接口和编译期多态更重要,此时的doProcessing内的w
- 必须支持哪种接口由template中执行于w身上的操作来决定,看起来T必须支持size,normalize和swap函数,copying构造函数,不等比较,但这并不完全这确。通过表达式推断出来的函数接口便是T必须支持的一组隐式接口
- 凡涉及w的任何函数调用,例如operator>和operator!=,有可能造成template实例化使调用成功,此行为发生在编译期。以不同模板参数实例化函数模板会导致调用不同的函数,这就是编译期多态
template<typename T>
void doProcessing(T& w)
{
if (w.size() > 10 && w != someNasyWidget) {
T temp(w);
temp.normalize();
temp.swap(w);
}
}
- 编译期多态和运行期多态的区别类似于重载函数的调用(编译期)和虚函数的动态绑定(运行期),显式接口和隐式接口的区别比较新颖,通常显式接口由函数的签名式(函数名称、参数类型、返回值)构成,如下面的Widget class,public接口由一个构造函数、一个析构函数、函数size、normalize、swap及其参数类型、返回类型、常量性(constness)构成,还包括编译器产生的copy构造函数和copy assignment操作符,另外也可以包括typedef或者其他public成员变量
class Widget {
public:
Widget();
virtual ~Widget();
virtual std::size_t size() const;
virtual void normalize();
void swap(Widget& other);
...
};
- 隐式接口则是由有效表达式组成,再看看doProcessing一开始的条件,看起来似乎必须提供一个返回整数值的size函数,支持一个operator!=函数用来比较两个T对象,但并非如此。T必须支持size成员函数,但这个函数可能从基类继承,它不需要返回一个整数值,甚至不需要返回一个数值类型,甚至不需要返回一个定义有operator>的类型。它唯一要做的就是返回一个类型为X的对象,而X对象加上一个int必须能够调用一个operator>。operator>不一定要取一个类型为X的参数,它也可以取类型为Y的参数,只要存在一个X到Y的隐式转换。同理T不需要支持operator!=,只要T可被转换成X而someNasyWidget可被转换成Y(当然,不考虑operator&&被重载)
template<typename T>
void doProcessing(T& w)
{
if (w.size() > 10 && w != someNasyWidget) {
...
42 了解typename的双重意义
- template声明式中class和typename的意义完全相同
template<class T> class Widget;
template<typename T> class Widget;
- 但有时一定要用typename。假设有一个函数模板,接受一个STL兼容容器为参数,容器内对象可被赋值为int
// 打印容器第二个元素,该代码不合法,后续说明原因
template<typename C>
void print2nd(const C& container)
{
if(container.size() >= 2) {
C::const_iterator iter(container.begin());
++iter;
int value = *iter;
std::cout << value;
}
}
- iter的类型是 C::const_iterator,实际的类型取决于参数C。template内出现的名称如果相依于某个template参数,称之为从属名称,如果从属名称在 class 内呈嵌套状,我们称它为嵌套从属名称,如C::const_iterator,实际上它还指涉某种类型,所以还是嵌套从属类型名称。另一个local变量value是int类型,并不依赖任何模板参数,称为非从属名称。嵌套从属名称可能会导致解析困难
template<typename C>
void print2nd(const C& container)
{
C::const_iterator* x;
...
}
- 在知道C::const_iterator是类型的前提下,上面的代码看起来没问题,但如果C有个static成员变量碰巧命名为 const_iterator,或x碰巧是一个global变量名称,那么里面的 * 可能不是指针,而是乘号。在知道C是什么之前,无法知道C::const_iterator是否为类型,解析器在template中遭遇一个嵌套从属名称时,它便假设其不是类型,因此缺省情况下嵌套从属名称不是类型。现在可知,iter只有在C::const_iterator是个类型时才合理,而我们没有指出,所以C++就假设它不是。因此如果想在template中指涉一个嵌套从属类型名称,就必须在紧邻它的前一个位置放置关键字typename
// 合法的代码
template<typename C>
void print2nd(const C& container)
{
if (container.size() >= 2) {
typename C::const_iterator iter(container.begin());
...
- 有个例外,typename不能出现在基类列表内的嵌套从属名称之前,也不能在成员初值列中作为base class修饰符
template<typename T>
class Derived: public Base<T>::Neted { // base class list中不允许typename
public:
explicit Derived(int x) : Base<T>::Nested(x) // 成员初值列中不允许typename
{
typename Base<T>::Nested temp; // 除了前两种情况嵌套从属名称都得加上typename
...
}
...
};
- 嵌套从属名称很长时,若要使用typedef,在typename前面加上typedef
template<typename IterT>
void workWithIterator(IterT iter)
{
typename std::iterator_trains<IterT>::value_type temp(*iter);
...
}
// 加上typedef
template<typename IterT>
void workWithIterator<IterT iter)
{
typedef typename std::iterator_trains<IterT>::value_type value_type;
value_type temp(*iter);
...
}
43 学习处理模板化基类内的名称
- 继承模板基类调用基类函数时,派生类拒绝在基类内寻找继承而来的名称,因为模板参数到后来才确定,派生类此前无法知道基类是什么,更无法明确知道基类是否有该函数存在
template<typename T>
class A {
public:
void f() {
...
}
...
private:
...
};
template<typename T>
class B : public A<T> {
public:
void useF(){
f(); // 调用基类函数,编译错误,编译器无法在模板化基类中查找f
}
...
private:
...
};
- 有三个解决方法,第一是在base class函数调用动作之前加上this->,第二是使用using声明式,第三是明确指出被调用的函数位于base class内,第三种做法的缺点是如果调用的是虚函数会关闭动态绑定
// 在base class函数调用动作之前加上this->
template<typename T>
class B : public A<T> {
public:
void useF(){
this->f();
}
...
};
// 使用using声明式
template<typename T>
class B : public A<T> {
public:
using A<T>::f;
void useF(){
f();
}
...
};
// 明确指出被调用的函数位于base class内,这种做法的缺点是如果调用的是虚函数会关闭动态绑定
template<typename T>
class B : public A<T> {
public:
void useF() {
A<T>::f();
}
...
};
44 将与参数无关的代码抽离templates
- 模板是节省时间和避免代码重复的一个很好的办法,但如果不小心可能会导致代码膨胀:其二进制带着重复的代码,数据,或两者,结果可能是源码看起来合身而整齐,但目标码却不是那么回事,避免这样的二进制浮夸的主要工具是共性与变性分析
- 编写某个函数,发现某些部分的实现和另一个函数相同,抽出共同部分放进第三个函数,然后令原先两个函数调用这个新函数。同理,如果是class,把共同部分搬移到新class,然后使用继承或复合,令原先的 class取用这共同特性,原class的互异部分留在原位。但在模板代码中重复是隐晦的,毕竟只存在一份模板源码,所以必须训练自己感受当模板被多次实例化时可能发生的重复
// A是一个n * n矩阵,元素是类型为T的对象
template <typename T, int n>
class A {
public:
void f();
};
// 下面会实例化两份f,引起代码膨胀
A<double, 5> a;
a.f();
A<double, 10> b;
b.f();
template <typename T>
class A {
protected:
void f(int n);
};
template <typename T, int n>
class B : private A<T> { // 基类只是为了帮助派生类实现而非is-a关系,所以用private继承
private:
using A<T>::f;
public:
void f() { this->f(n); }
};
- 但还有些问题,派生类如何告诉基类数据在哪,A::f如何知道该操作什么数据?方法是令A贮存一个指针指向n所在的内存
template <typename T>
class A {
protected:
A(int n, T* pMem) : number(n), pData(pMem) {}
void setDataPtr(T* ptr) { pData = ptr; }
...
private:
int number;
T* pData;
};
- 这允许派生类决定内存分布方式。某些实现版本也会将数据存在B对象内部
template <typename T, int n>
class B : private A<T> {
public:
B() : A<T>(n, data) {}
...
private:
T data[n*n];
};
- 这种类型的对象不需要动态分配内存,但对象自身可能非常大。还有一种做法是通过new分配内存,把每一个矩阵的数据放进heap
template <typename T, int n>
class B : private A<T> {
public:
B() : A<T>(n, 0), pData(new T[n*n])
{ this->setDataPtr(pData.get()); }
...
private:
boost::scoped_array<T> pData;
};
45 运用成员函数模板接受所有兼容类型
class Top { ... };
class Middle : public Top { ... };
class Bottom : public Middle { ... };
Top* pt1 = new Middle; // 将Middle*转换成Top*
Top* pt2 = new Bottom; // 将Bottom*转换成Top*
const Top* pct2 = pt1; // 将Top*转换成const Top*
- 在自定的智能指针中模拟上述行为,我们希望以下代码通过编译
template<typename T>
class SmartPtr {
public:
explicit SmartPtr(T* realPtr);
...
};
SmartPtr<Top> pt1 = SmartPtr<Middle>(new Middle);
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);
SmartPtr<const Top> pct2 = pt1;
- 同一个模板的不同实例之间不存在固有关系,继承关系的两类型分别实例化某个模板,产生出的两个实例并不带有继承关系,所以编译器视SmartPtr<Middle>和SmartPtr<Top>为完全不同的classes,为了获得转换能力必须将它们明确地编写出来,我们需要的不是写一个构造函数,而是写一个构造模板,称为member function template,其作用是为class生成函数
template<typename T>
class SmartPtr {
public:
template<typename U> // member template
SmartPtr(const SmartPtr<U>& other); // 为了生成copy构造函数
...
};
- 以上代码的意思是,任何类型T和任何类型U,可以根据SmarPtr<U>生成一个SmartPtr<T>,这一类构造函数通过对象u创建对象t,我们称之为泛化copy构造函数。为了效仿隐式转换,它并未声明为explicit。完成声明后必须从某方面对member template所创建的成员函数群进行筛除,我们希望根据一个SmartPtr<bottom>创建一个SmartPtr<top>,却不希望反过来,也不希望根据一个SmartPtr<double>创建一个SmartPtr<int>,因为现实中并没有将double*转换成为int*的隐式转换。假设SmartPtr和shared_ptr一样也提供一个get成员函数返回智能指针原本持有的指针,由此可以在构造模板实现代码中约束转换行为
template<typename T>
class SmartPtr {
public:
template<typename U>
SmartPtr(const SmartPtr<U>& other)
: heldPtr(other.get()) { ... }
T* get() const { return heldPtr; }
...
private:
T* heldPtr; // 该智能指针持有的原始指针
};
- 成员初值列中,以类型为U*的指针初始化类型为T*成员变量,这个行为只有当“存在某个隐式转化可将一个U*指针转换为一个T*指针”时才能通过编译,这正是我们的目的。现在SmartPtr<T>有了一个泛化copy构造函数,这个构造函数只在其所获得的实参隶属适当(兼容)类型时才通过编译
- member template并不改变语言规则,程序仍需要一个copy构造函数,如果没声明,编译器会生成一个。声明一个泛化copy构造函数并不阻止编译器生成它们自己的copy构造函数,所以要控制copy构造函数的方方面面,必须同时声明泛化copy构造函数和正常的copy构造函数,赋值操作符同理
template<class T>
class shared_ptr {
public:
shared_ptr(shared_ptr const& r); // copy构造函数
template<class Y>
shared_ptr(shared_ptr<Y> const& other); // 泛化copy构造函数
shared_ptr& operator= (shared_ptr const& r); // copy assignment
template<class Y>
shared_ptr& operator= (shared_ptr<Y> const& r); // 泛化copy assignment
};
46 需要类型转换时请为模板定义非成员函数
- 条款24提到过为什么non-member函数才有能力“在所有实参身上实施隐式类型转换”,本条款将Rational class模板化
template<typename T>
class Rational {
public:
Rational(const T& numerator = 0, const T& denominator = 1);
const T numerator() const;
const T denominator() const;
...
};
template<typename T>
const Rational<T> operator* (const Rational<t>& lhs, const Rational<t>& rhs)
{
...
}
Rational<int> oneHalf(1,2);
Rational<int> result = oneHalf * 2; // 编译错误
- 条款24内,编译器知道调用operator*,但这里编译器想知道operator*的template实例化什么函数,必须先算出T是什么,以oneHalf进行推导,第一个实参类型是Rational<int>,所以T一定是int,但第二个实参类型是int,编译器无法由此推算出T。template实参推导过程中不会考虑隐式转换,所以编译器也不会使用构造函数将2转换为Rational<int>进而将T推导为int
- 解决方法是,template class内的friend声明式可以指涉某个特定函数,class Rational<T>可以声明operator*是它的一个friend函数,class template并不依赖template实参演绎,所以编译器总是能在class Rational实例化时得知T
template<typename T>
class Rational {
public:
...
// class template内,template名称就是template和其参数的简写
// 因此不必写Rational<T>,简写节省时间而且代码更干净
friend
const Rational operator* (const Rational& lhs, const Rational& rhs);
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{ ... }
- 现在对operator*的混合式调用可以通过编译了,当对象oneHalf被声明为一个Rational<int>,class Rational<int>被实例化,friends函数operator*(接受Rational<int>参数)也跟着被自动声明出来。friend函数身为一个函数而非函数模板,因此编译器可在调用它时使用隐式转换函数。不过这段代码虽然能通过编译,却无法连接,函数声明于Rational内并没有被定义,通过外部的operator* template提供定义式是没用的,它相当于自己声明的一个函数,没有提供定义式,连接器当然找不到它,解决方法就是将operator*函数本体合并至声明式中
template<typename T>
class Rational {
public:
...
// Rational<T>都简写为Rational
friend
const Rational operator* (const Rational& lhs, const Rational& rhs)
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
};
- 这里使用friend不同于传统用途“访问class的non-public成分”。为了让类型转换可能发生在所有实参上,需要non-member函数,为了令这个函数被自动实例化,需要将它声明在class内部,而在class内部声明non-member函数的唯一办法就是friend,因此我们这样做了
- 定义于class内的函数都暗自成为inline,包括operator*这样的friend函数。将inline声明的开销最小化的做法是令operator*不做任何事情,只调用一个定义于class外部的辅助函数。Rational是个模板意味着这个辅助函数通常也是一个模板,模板定义应当放进头文件内
template<typename T> class Rational;
template<typename T>
const Rational<T> doMultiply (const Rational<T>& lhs, const Rational<T>& rhs)
{
return Rational<T>(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
template <typename T>
class Rational {
public:
...
friend
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{ return doMultiply(lhs, rhs); }
...
};
47 请使用traits classes表现类型信息
- STL主要由“用以表现容器、迭代器和算法”的template构成,但也覆盖若干工具性的template,其中一个名为advance,用来将某个迭代器移动某个给定距离
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d); // 将迭代器向前移动d个单位,d<0则向后移动
- STL有五种迭代器分类
- Input迭代器只能向前移动,一次一步,只读,且只能读取一次,它们模仿指向输入文件的阅读指针,istream_iterator是这一分类的代表
- Output迭代器类似,但一切只为输出,只能向前移动,一次一步,只能写,且只能涂写一次,它们模仿指向输出文件的涂写指针,ostream_iterator是代表。这两种是威力最小的迭代器,它们只适合一次性操作算法
- forward迭代器,可以做前述两种分类所能做的每一件事,而且可以读或写其所指物一次以上,因此可施行于多次性操作算法。STL未提供单向linked_list,但某些程序库有,指入这种容器的迭代器就属于forward迭代器。
- Bedirectional迭代器比上一个威力更大:除了可以向前移动,还可以向后移动。STL的list,set,multiset,map和multimap的迭代器都属于这一类
- random access迭代器威力最大,可以执行迭代器算术,常量时间内可以向前或向后跳跃任意距离。内置指针也可以当random迭代器用。vector,deque和string提供的迭代器都是这一类
- 这5种分类,c++标准程序库分别提供专属的tag struct加以确认,这些struct之间的继承关系是有效的is-a关系,所有forward迭代器都是input迭代器,依此类推
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_iterator_tag : public bidirectional_iterator_tag {};
template<typename IterT, typename DistT>
void advance(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++程序员共同遵守的协议。这个技术的要求之一是,它对内置类型和用户自定义类型的表现必须一样好
- iterator_traits 的运作方式是对于每一个 IterT 类型,在struct iterator_traits<IterT>中声明一个名为iterator_category 的 typedef,这个 typedef用来确认IterT的迭代器分类。iterator_traits 通过两部分实现这一点。首先,它强制要求任何用户定义迭代器类型必须嵌套一个名为 iterator_category的typedef,用来确认适合的tag struct。例如,deque的迭代器可随机访问,所以一个deque iterator的class看起来就像这样
template < ... >
class deque {
public:
class iterator {
public:
typedef random_access_iterator_tag iterator_category;
...
};
...
};
- 然而,list的iterator是双向的,所以它们是这样的
template < ... >
class list {
public:
class iterator {
public:
typedef bidirectional_iterator_tag iterator_category;
...
};
...
};
- 而iterator_traits只是简单地模仿了iterator class的嵌套typedef
template<typename IterT>
struct iterator_traits {
typedef typename IterT::iterator_category iterator_category;
...
};
- 以上这种方法对于用户自定义类型行得通,但是对于指针(也是一种迭代器)却行不通,因为指针不可能嵌套typedef。为了支持指针迭代器,iterator_traits特别针对指针类型提供一个局部特化版本。由于指针的行径与ramdom access迭代器类似,所以iterator_traits为指针指定的迭代器类型如下
template<typename IterT>
struct iterator_traits<TierT*> {
typedef random_access_iterator_tag iterator_category;
...
};
- 到此为止,你了解了如何设计和实现一个 traits class
- 识别你想让它可用的关于类型的一些信息,例如,对于iterator来说,就是它们的iterator category
- 为该信息选择一个名称,如iterator_category
- 提供一个 template和一组specialization(如iterator_traits),内含你希望支持的类型的信息
- 给出了iterator_traits(实际上是 std::iterator_traits)就可以改善 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))
...
}
- 虽然看起来合理,但它不是我们想要的。首先它会导致编译问题,这点到条款48再讨论。现在有一个更根本的问题,IterT的类型在编译期间是已知的,所以iterator_traits<IterT>::iterator_category也可以在编译期间被确定。但if语句运行时才会确定,运行时才做编译期间就能做的事情不仅浪费了时间,还可能造成执行码膨胀。我们想要一个条件式判断“编译期核定成功”之类型,取得这种行为的办法就是重载。为了让advance的行为如我们所期望,我们需要做的是产生两版重载函数,内含advance的本质内容,但各自接受不同类型的iterator_category对象,这里将这两个函数取名为doAdvance
template <typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, random_access_iterator_tag)
{
iter += d;
}
template <typename IterT, typename DistT>
void doAdcance(IterT& iter, DistT d, bidirectionl_iterator_tag)
{
if(d >= 0) { while (d--) ++iter; }
else { while (d++) --iter; }
}
template <typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, input_iterator_tag)
{
if (d < 0) {
throw out_of_range("Negative distance");
}
while (d--) ++iter;
}
- 由于forward_iterator_tag继承自input_iterator_tag,因此针对input_iterator_tag的doAdvance版本也能处理 forward iterator,这就是在不同的iterator_tag struct之间继承的动机。有了上面定义的这些重载的函数,那么advance需要做的只是调用它们并额外传递一个对象
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d)
{
doAdvance(
iter, d,
typename
std::iterator_traits<IterT>::iterator_category()
);
}
- 现在能够概述如何使用一个 traits class 了:
- 创建一套重载的 "worker" function或者 function template(如doAdvance),它们的差异只在于traits参数。令每个函数实现码与接受的traits信息一致地实现
- 创建一个 "master" function或者 function templates(如advance)调用上条的"worker" function,传递通过 traits class所提供的信息
48 认识template元编程
- 模板元编程(template mataprogramming,TMP)是编写C++程序并执行于编译期的过程,所谓template mataprogram(模板元程序)是以C++写成,执行于C++编译器内的程序,一旦TMP程序结束执行,其输出,也就是从templates具现出来的若干C++源码,便会一如往常地被编译
- TMP于1990s初期被发现,自从template加入C++,TMP底层特性便被引进了。TMP有两个伟大效力,第一它让某些事情变得更容易,第二template program执行于编译期,工作可以从编译期转移至执行期,某些错误可以被提前侦测,另外使用TMP的程序在每一方面都更高效,较小的可执行文件,较短的运行期,较少的内存,代价是编译时间变长了,再来看看条款47中的advance
template<typename Iter, typename DistT>
void advance(IteT& iter,DistT d)
{
if(typeid(typename std::iterator_traits<IterT>::iterator_category)
== typeid(std::random_access_iterator_tag)) {
iter += d;
}
else {
if(d >= 0){ while(d--) ++iter; }
else { while(d++) --iter; }
}
}
- typeid-based解法效率比traits解法低,因为在此方案中,类型测试发生在运行期而不是编译期,并且运行期类型测试代码会被连接在可执行文件中。TMP比正常的C++程序更高效,traits解法就是TMP,一些东西在TMP比在正常的C++更容易,advance提供一个好例子。advance的typeid-based实现方式可能导致编译期问题
std::list<int>::iterator iter;
...
advance(iter, 10); // 移动iter向前走10个元素
// 上述实现无法通过编译
// 下面这一版的advance便是针对上述调用产生的
// 将template参数iterT和DistT分别替换为iter和10的类型之后得到
void advance(std::list<int>::iterator& iter, int d)
{
if (typeid(typename std::iterator_traits<std::list<int>::iterator>::iterator_category)
==typeid(std::random_access_iterator_tag)) {
iter += d; // 错误
}
else {
if(d >= 0) { while(d--) ++iter; }
else { while(d++) --iter; }
}
}
- 问题出在+=操作符,list::iterator是bidirectional迭代器,不支持+=,只有random access迭代器才支持+=。测试typeid的那一行总是会因为list<int>::iterator而失败,所以不会执行+=那一行,但编译器必须确保所有源码都有效,即使是不会执行的代码。而traits-based TMP解法针对不同类型执行不同代码,被拆分为不同函数,不会出现上述问题
- TMP已被证明是个图灵完全机器,也就是说它的威力足以计算任何事物。可以使用TMP声明变量、执行循环、编写调用函数......为了再次简单认识事物在TMP中如何运作,来看看循环,TMP没有真正的循环构件,循环由递归完成。TMP的递归不涉及递归函数调用,而是涉及递归模板实例化。下面是一个TMP例子,编译期计算阶乘,示范如何通过递归模板具体化实现循环,以及如何在TMP中创建和使用变量
template<unsigned n>
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<n>内部指涉另一个template实例Factorial<n-1>之时,template特化版本Factorial<0>是递归的结束。每个Factorial template实例都是一个struct,每个struct都使用enum hack声明一个名为value的TMP变量,用来保存当前计算所得的阶乘值。TMP以递归模板实例化取代循环,每个实例有自己的一份value,每个value有其循环内适当值
// 可以这样使用Factorial
int main()
{
std::cout << Factorial<5>::value; // 印出120
std::cout << Factorial<10>::value; // 印出3628800
}
- 用Factorial示范TMP就像用hello world示范编程语言一样。为了领悟TMP之所以值得学习,就要先对它能够达成什么目标有一个比较好的理解,举三个例子
- 确保量度单位正确。使用TMP可以确保在编译期所有量度单位的组合都正确,不论其计算多么复杂
- 优化矩阵运算
- 可以生成客户定制之设计模式实现品。使用policy-based design之TMP-based技术,有可能产生一些template用来表述独立的设计选项,然后可以任意结合它们,导致模式实现品带着客户定制的行为
网友评论