继承与组合提供了重用对象代码的方法,而C++的模板特征提供了重用源代码的方法。在C++中,模板实现了参数化类型的概念。
这一节我不介绍模板的初级语法,读者可自行阅读《C++编程思想 第一卷》,那里还介绍了容器(container)和迭代器(iterator)的概念,他们是面向对象程序设计的基本构件,值得阅读。这里介绍一些模板的高级语法。
不得不说这节的内容有些多,但是我已经尽最大努力整合了这些知识,本节还介绍了一个C++模板的高级特性——模板元编程(template metaprogram),它意味着产生了一种支持编译时编程的机制,耐心点吧!
1、模板参数
模板有两类:函数模板和类模板。二者都由他们的参数来完全的描绘模板的特性。每个模板参数描述了下述内容之一:
1)类型(或者是系统固有类型或者是用户自定义类型)。
2)编译时常数值。
3)其他模板。
下面是一个简单地Stack容器类。作为容器,Stack对象与容器中存储的对象的类型毫无关联;持有对象的逻辑独立于所持有的对象的类型。基于这个原因,可以用一个类型参数来代表所包含的类型:
template <class T>
class Stack
{
T *data;
size_t count;
public:
void push(const T& t);
//Etc
};
某个特定的Stack实例所使用的实际类型,由参数T的实参类型来决定:
Stack<int> myStack;
编译器通过用int来代替T生成相应的代码,从而提供了一个Stack的int版。在这个例子中,由模板生成类的实例的名字是Stack<int>。
1.1 无类型参数模板
一个无类型模板参数必须是一个编译时所知的整数值(一定是整数)。举个例子,可以创建一个固定长度的Stack,指定一个无类型参数作为其中基础数组的大小:
template <class T, size_t N>
class Stack
{
T data[N];
size_t count;
public:
void push(const T& t);
//Etc
};
当需要一个模板实例时,必须为参数N提供一个编译时常数值,例如:
Stack<int, 100> myFixedStack;
由于N的值在编译时是已知的,内含的数组(data)可以被置于运行时堆栈,而不是动态存储空间。
1.2 默认模板参数
在类模板中,可以为模板参数提供默认(缺省)参数,但是在函数模板中却不行。作为默认的模板参数,他们只能被定义一次,编译器会知道第1次的模板声明或定义。一旦引入了一个默认参数,所有它之后的模板参数也必须具有默认值。如下所示:
template <class T = int, size_t N = 100>
class Stack
{
T data[N];
size_t count;
public:
void push(const T& t);
//Etc
};
Stack<> myStack;
默认参数大量用于标准C++模板库(STL)中。比如vector类模板的声明如下:
Template<class T, class Allocator = allocator<T> >
Class vector;
alloctor是一个空间配置器,C++基础还不错的读者可以去阅读侯捷的著作《STL源码剖析》,里面有关于空间配置器,容器,迭代器,算法的详细介绍。
注意,在比较低级的编译器中,在最后两个尖括号字符之间有空格,高级编译器则不用,但为了移植性,还是把空格加上吧,这就避免了编译器将那两个字符(> >)解释为右移运算符。
1.3 模板类型的模板参数
模板还可以接受的一种模板参数是另一个类模板。下面的例子说明了一个模板类型的模板参数:
#include <iostream>
#include <vector>
#include <list>
using namespace std;
template <class T, template<class T, class = allocator<T> > class Seq>
class Container : public Seq < T >
{
public:
void show()
{
iterator it = begin();
while (it != end())
{
cout << *it++ << endl;
}
}
};
void main()
{
Container<int, vector> vc;
vc.push_back(1);
vc.push_back(2);
vc.push_back(3);
vc.show();
Container<int, list> lc;
lc.push_back(1);
lc.push_back(2);
lc.push_back(3);
lc.show();
}
关于
多态与继承方面的知识,读者可以去阅读《
C++
编程思想》,也可以查看我所总结的多态与继承一节,这里对上面的例子就不做过多介绍了,
vector
、
list
都是
STL
中的容器,读者可以阅读《
STL
源码剖析》。
Container模板包含两个参数:一个参数是它持有的类对象类型,还有一个参数是它持有的类对象类型的序列数据结构。如果还没有声明Seq是一个模板类型的模板参数,编译器就不会在这里将Seq解释为一个模板。在main()中使用了一个持有整数的vector和一个持有整数的list将Container实例化。
在本例Container的声明中对Seq的参数进行命名不是必须的,所讨论的这一行是:
template <class T, template<class T, class = allocator<T> > class Seq>
也可以这样写:
template <class T, template<class T, class _A= allocator<T> > class Seq>
这种情况类似于某些时候省略函数参数的名称,当不需要他们的时候就可以省略掉。
注意,当编译器看到模板型模板参数的内部参数时,无法顾及到默认参数,因此为了得到一个确切的匹配,必须重复声明默认参数。
1.4 typename关键字
这里关于typename关键字不做过多介绍,有两点很重要:
1)创建一个新类型
一定不能认为关键字typename创建了一个新类型名。它确实没有,它的目的就是通知编译器,被限定的那个标识符应该被解释为一个类型,而不是对象或者其他什么。
typename Seq<T>::iterator It;
2)用typename代替class
关键字typename另一个重要的作用是,可以在模板定义的模板参数列表中选择使用typename代替class,对大多数程序而言,这种描述方式更加一目了然。
1.5 成员模板
成员模板:在另一个类或者类模板中声明的一个模板。标准C++库中的complex类模板就是一个有用的例子。Complex模板有一个类型参数,它代表一个拥有复数的实部和虚部的基础浮点类型。下面的代码片段是从标准库中摘录出来的,它说明了在complex类模板中的成员模板构造函数:
template<typename T>
class complex
{
public:
template<class X>
complex(const complex<X> &);
};
上面的成员模板构造函数创建了一个新复数,这个复数使用了另外一个浮点类型作为它的基类型,如下所示:
complex<float> z(1, 2);
complex<double> w(z);
在w的声明中,complex模板参数T是double类型,X是float类型。成员模板是的这种灵活的变换更加容易。
在模板中定义另一个模板是一种嵌套操作,如果想在外部类的定义之外定义成员模板,那么作为引入模板的前缀必须能够反映这种嵌套。例如,如果要实现complex类的模板,还想再complex模板类定义之外定义成员模板构造函数,可以如下定义:
template<typename T>
template<typename X>
complex<T>::complex(const complex<X> &c)
{
//Body
}
标准库中成员函数模板的另一个应用是在容器的初始化中。假设有一个int型的vector,要想初始化一个新的double型的vector,如下所示:
int data[5] = {1, 2, 3, 4};
vector<int> v1(data, data+5);
vector<double> v2(v1.begin(), v1.end());
只要v1中的元素与v2中的元素类型兼容即可,vector类模板有如下成员模板构造函数:
template<class InputIterator>
vector(InputIterator first, InputIterator last, const Allocator& = Allocator());
成员模板也可以是类,成员模板函数不能声明为virtual类型。当今的编译器技术在解析一个类时,希望知道这个类的虚函数表的大小。如果允许虚函数成员模板函数的存在,则需要提前知道程序中所有这些成员函数的调用放在什么地方,因为编译器要实例化这些函数。这是很不灵活的,尤其是在多文件项目中。
2、有关函数模板的几个问题
正如一个类模板描述了一族类,一个函数模板描述了一族函数。当在实例化类模板时总是需要使用尖括号并且提供所有的非默认模板参数。然而,对于函数模板,经常可以省略掉模板参数,甚至根本不允许使用默认模板参数。仔细看一下<algorithm>头文件(这个头文件很有用,里面有许多算法的实现)中声明的min()函数模板的实现,如下所示:
template<typename T>
const T& min(const T& a, const T& b)
{
return (a>b)? a:b;
}
可以通过提供尖括号里面的参数类型来调用这个模板,正如对类模板的操作一样,如下所示:
int z = min<int>(i, j);
这个语法告诉编译器,min()模板需要在参数T的位置使用int来进行特化,这样编译器就会产生相应的代码。
2.1 函数模板参数类型的推断
可以像上面的例子一样,一直使用这样明确的函数模板特化方式,但是如果可以不明确指定模板参数类型,而让编译器从函数的参数中推断出他们的类型会更加方便如下所示:
int z = min(i, j);
如果i和j都是int类型,编译器就会知道程序需要的是min<int>(),之后会自动进行实例化。由于在模板定义时指定了唯一的模板类型参数用于函数的两个参数,因此这两个参数的类型必须一致。C++系统不提供标准转换。若想在两个不同类型的参数(一个int型和一个double型)中找出其中的最小值,下面的这种min()调用将会出错:
int z = min(x, y); //x is a double
要解决这个问题,可以将一个参数的类型转换为另一个参数的类型,或者恢复到完全说明调用语法,如下所示:
int z = min<double>(x, y);
该语句告诉编译器产生double版本的min(),之后j通过标准类型转换规则向上转型成double型数据。
也可以要求min()的两个参数完全独立,如下所示:
template<typename T, typename U>
const T& min(const T& a, const U& b)
{
return (a>b)? A:b;
}
这通常会是一个好办法,但是由于min()必须返回一个值,却没有一个理想的方式来决定这个返回值的类型到底是T还是U,因此这里的这个“好办法”还是有问题的。
若一个函数模板的返回类型是一个独立的模板参数,当调用它的时候就一定要明确指定它的类型,因为这时已经不能从函数参数中推断出他的类型了,看一下这个著名的模板:
#include <iostream>
using namespace std;
template <typename R, typename P>
R implicit_cast(const P &p)
{
return p;
}
void main()
{
int i = implicit_cast<double>(3.14);
cout << i << endl; //3
//char *p=implicit_cast<char *>(i);
}
注意,如果有一个函数模板,他的模板参数即作为参数类型,又作为返回类型,那么一定要首先声明函数的返回类型参数,否则就不能省略掉函数参数列表中的任何类型参数。
如果将上面例子中模板参数列表中的R和P交换一下,这个程序将不能通过编译,这是因为没有指定函数的返回类型。最后一行(被注释掉的)也是不合法的用法,原因是没有从int到char*的标准类型转换。
2.2 函数模板重载
这里就不详细说明了,只要编译器能区分开,就可以使用重载同名的模板。
2.3 以一个已生成的函数模板地址作为参数
在很多情况下需要获得一个函数的地址。而这个函数有可能就是由一个函数模板生成的,因此需要以某种方式来处理这种以函数模板的地址做参数的情况:
template<typename T>
void f(T *)
{}
void h(void(*pf)(int *))
{}
template<typename T>
void g(void(*pf)(T*))
{}
void main()
{
h(&f<int>);//full type specification
h(&f);//type deduction
g<int>(&f<int>); //full type specification
g(&f<int>);//type deduction
g<int>(&f);//partial( but sufficiend ) specification
}
这个例子说明了几个问题。首先,既然使用模板,所有的标识就必须匹配。其次,拥有一个函数指针作为参数的函数本身可以是一个模板。
也可以在main()中看到类型推断。第1个对h()的调用明确的给出了f()的模板参数,但由于h()规定只接收具有int*参数的函数地址做参数,依次第2个调用由编译器来推断类型。至于g(),它的情况就更加有趣了,因为它在其中引用了两个模板。如果什么都不给,编译器就推断不出类型;但说明了一个int,或者赋予f()或者赋予g(),余下的类型编译器自己就能够推断出来。
在使用transform算法时,其中的第4个参数就是一个函数指针,而这个参数很可能就是<cctype>中的模板函数tolower或toupper,必须小心使用,其中的问题请阅读《C++编程思想 第二卷》中的这个部分。
2.4 函数模板的半有序
在缺少普通函数时,对函数模板进行重载有可能引起二义性的情况,即不知选择哪个模板。为了将发生的这种情况的几率减到最低,系统为这些函数模板定义了次序,在生成模板函数的时候,编译器将从这些函数模板中选择特化程度最高的模板(如果有这种模板的话)。一个函数模板要考虑多种特化,在这些特化的模板中对于某个特定的函数模板来说,如果每一种可能的参数列表的选择都能够匹配该模板的参数列表,那么,这些可能的参数列表选择也都能够匹配另一个函数模板的参数列表,但反过来却不成立。请看下面的函数模板声明,取自C++标准文档:
template<class T> void f(T);
template<class T> void f(T *);
template<class T> void f(const T*);
任何类型都可以匹配第1个模板。第2个模板比第1个模板的特化程度更高,因为只有指针类型才能够匹配它。换句话说,可以把匹配第2个模板的一组可能的函数调用看成是匹配第1个模板的子集。上面的第2个模板和第3个模板的声明也存在类似的关系。下面的程序说明了这些规则:
#include <iostream>
using namespace std;
template <class T> void f(T){ cout << "T" << endl; }
template <class T> void f(T *){ cout << "T *" << endl; }
template <class T> void f(const T *){ cout << "const T *" << endl; }
void main()
{
int i = 0;
f(0); //T
f(&i); //T *
f((const int*)(&i)); //const T *
}
f(&i)调用和第1个模板匹配,但由于第2个模板的特化程度更高,因此这里调用了第2个模板。
3、模板特化
术语特化(specialization)在C++中有一个特别的与模板相关的含义。从本质上说,一个模板定义就是一个实体一般化(generalization)的过程,给定模板参数时,这些模板参数决定了这一模板的许多可能的实例中的一个独一无二的实例,因此这样的结果就被称为模板的一个特化。开始介绍的min()函数模板是一个寻找最小值函数的一般化,因为没有指定它的参数类型。若为这个模板参数提供了类型,不管它是明确给定的还是通过参数推断获得的,由编译器生成的结果代码(例如,min<int>())都是这个模板的一个特化。生成的代码也被认为是这个模板的一个实例化(instantiation),就像是由模板工具完全生成它的整个代码体一样。
3.1 显示特化
编程人员也可以自己为一个模板提供代码来使其特化,采用这种方法进行编码的程序设计人员越来越多。类模板经常需要程序员来为他提供模板特。
下面这个例子摘自《C++名家对话》,没有使用《C++编程思想》上的例子,因为这有一个需要注意的地方,值的研究一下:
#include <iostream>
using namespace std;
template<typename T, size_t size = 10>
class c
{
T m[size];
public:
c()
{
cout << "c<char>" << endl;
}
void print_size()
{
std::cout << size << std::endl;
}
};
template<>
class c < char >
{
char m[100];
public:
c()
{
cout << "c<char>" << endl;
}
void print_size()
{
std::cout << 100 << std::endl;
}
};
int main()
{
c<char> c1;
c<char, 10> c2;
c1.print_size(); //100
c2.print_size(); //100
return 0;
}
前缀“
template<>
”告诉编译器接下来的是一个模板的特化。类模板显示特化往往比函数模板显示特化更有用。当程序员为一个类模板提供了一份完整的特化时,需要实现其中的所有成员函数。
考虑上面的例子,为什么c1和c2保持的数组大小都是100呢?c2里数组的大小难道不是10吗?这里要注意了,在特化模板类c时,模板c有一个默认(缺省)参数size=10,因此下面这条语句:
template<> class c<char>
相当于这条语句:
template<> class c<char, 10>
而选用哪个类模板来进行实例化的规则类似于函数模板的半有序规则——应该选择“特化程度最高”的模板,因此c2匹配的是特化版本,而定义c1时,这条语句:
c<char> c1;
也相当于这条语句:
c<char, 10> c1;
匹配的也是特化版本,数组大小当然都是100了。
3.2 半特化
类模板也可以半特化,vector<bool>限定了对象类型(bool类型),但并没有指定参数allocator的类型,声明如下:
Template<class Allocator>
class vector<bool, Allocator>;
用户可以提供一个自定义的allocator类型。
3.3 防止代码膨胀
这个小结的代码有点长,但是我觉得还是很有必要提出来的。
无论何时,一旦对某个模板进行了实例化,伴随着所有在程序中调用的该模板的成员函数,类定义中用于进行详尽描述的特化代码也就会生成。只有被调用的成员函数才生成代码。这是不错的。考虑这个例子:
class X
{
public:
void f()
{};
};
class Y
{
public:
void g()
{};
};
template<typename T>
class Z
{
T t;
public:
void a()
{
t.f();
}
void b()
{
t.g();
}
};
void main()
{
Z<X> zx;
zx.a(); //doesn't create Z<X>::b()
Z<Y> zy;
zy.b(); //doesn't create Z<Y>::a()
}
在代码注释中已经讲得很清楚了,当在程序中明确的为 zx 调用 Z<X>::a() 的时候,程序在编译时只能生成 Z<X>::a() 的代码,不会生成 Z<X>::b() 的代码。同样的道理,对 zy.b() 的调用也不会生成 Z<Y>::a() 。但是当类在进行第 1 次实例化时,如果所有的成员函数都生成了,就会使许多模板的使用明显的受到限制。
假设有一个模板化的Stack容器,现在想用int、int*和char*对它进行特化。这样将会生成3个版本的Stack代码,并链接为程序的一部分。使用模板的原因之一,首先就是不必手工赋值代码;但是代码仍然被复制了——只不过是编译器代替程序员完成了这个工作而已。可以结合使用完全特化和半特化模板,将指针类型存储到某个独立的类中这样可以减少程序实现的体积(因为指针的的存储大小都一样大,并且都可以类型转换为void*,一个指针类型的栈不需要区分指针是什么类型,这是程序员的事)。其关键是用void*进行完全特化,然后从void*实现中派生出所有其他的指针类型,这样共同的代码就可以共享了。程序如下:
//Stack.h
#ifndef STACK_H
#define STACK_H
#include <cassert>
#include <cstddef>
#include <cstring>
template<class T>
class Stack
{
private:
T* data;
std::size_t count;
std::size_t capacity;
enum{ INIT = 5 };
public:
Stack()
{
count = 0;
capacity = INIT;
data = new T[INIT];
}
void push(const T &t)
{
if (count == capacity)
{
std::size_t newCapacity = 2 * capacity;
T* newData = new T[newCapacity]; //1、分配新空间
for (size_t i = 0; i < capacity; ++i) //2、拷贝数据
{
newData[i] = data[i];
}
delete[]data; //3、释放空间
data = newData; //4、更新状态
capacity = newCapacity;
}
assert(count < capacity);
data[count++] = t;
}
void pop()
{
assert(count > 0);
--count;
}
T top()const
{
assert(count > 0);
return data[count - 1];
}
std::size_t size()const
{
return count;
}
};//Stack
template<>
class Stack < void * >
{
private:
void ** data;
std::size_t count;
std::size_t capacity;
enum{ INIT = 5 };
public:
Stack()
{
count = 0;
capacity = INIT;
data = new void*[capacity];
}
void push(void * const &t)
{
if (count == capacity)
{
size_t newCapacity = 2 * capacity;
void **newData = new void*[newCapacity];
for (size_t i = 0; i < capacity; ++i)
{
newData[i] = data[i];
}
capacity = newCapacity;
data = newData;
}
assert(count < capacity);
data[count++] = t;
}
void pop()
{
assert(count > 0);
count--;
}
void *top()const
{
assert(count > 0);
return data[count - 1];
}
std::size_t size()const
{
return count;
}
};//Stack<void *>
template<class T>
class Stack<T *> :private Stack < void * > //注意这里是私有继承
{
typedef Stack<void *> Base;
public:
void push(T* const &t)
{
Base::push(t);
}
void pop()
{
Base::pop();
}
T* top()const
{
return static_cast<T*>(Base::top()); //将void*类型转换为T*
}
std::size_t size()const
{
return Base::size();
}
};//Stack<T *>
#endif //STACK_H
这个简单地栈能根据需要进行容量的扩充。
void*
做成了一个优秀的完全特化版本。如前所述,
在一个类模板的特化中必然要实现所有的成员函数。由于仅仅想用
Stack<void *>
作为实现目标,而且也不希望将它的任何接口暴露给用户,因此用户定义的其他指针类型都私有继承自
Stack<void *>
。每个指针实例化后的成员函数都是
Stack<void *>
中相应函数的一个细微改进的函数。因而,无论何时对一个非
void*
类型的指针类型进行实例化,它产生的代码只是单独使用基本模板所产生的代码的一小部分
(
由于这个改进函数是内联的,因此不产生
Stack<void *>
的代码!
)
。下面是一个驱动程序:
#include "Stack.h"
#include <iostream>
using namespace std;
void main()
{
Stack<int> s1;
s1.push(1);
s1.push(2);
Stack<int *> s2;
int i = 3, j = 4;
s2.push(&i);
s2.push(&j);
//emptyTheStack()
}
4、名称查找问题
当编译器碰到一个标识符是,他必须能够确定这个标识符所代表的实体的类型和作用域。当编译器首次看到一个模板定义时它不知道有关这个模板的任何信息,只有当它看到模板的实例化时,他才能判断这个模板是否被正常的使用了。这种情况导致了模板编译需要分两个阶段进行。
这个小结的知识比较繁琐,有许多概念,读者需自行阅读《C++编程思想 第二卷》,这里只介绍一些重要的过程。
在第1阶段,编译器解析模板定义,寻找明显的语法错误,还要对它所能解析的所有名称进行解析。
模板编译的第2个阶段就是模板实例化,在这里,由编译器来决定是否使用模板的一个显示特化来取代基本的版本。
4.1 模板和友元
这一小结的内容基本都是语法层次上,读者自行阅读《C++编程思想 第二卷》,也许有些读者厌烦了这句话,但是这里又不得不讲,我将这篇文章的内容尽量缩减,基于语法层次上的东西希望读者自己去阅读。
很快就会进入模板的两个高级议题,所以继续吧!
5、模板编程中的习语
5.1 特征
这个小节的内容比较难理解,但在STL中应用很广泛,读者应该耐心着去体会体会。
特征模板技术,它是一种将与某种类型相关联的所有声明绑定在一起的实现方式。本质上说,使用特征技术,可以以一种灵活的方法从他们的语境中将这些类型和值进行“混合和搭配”,同时又使得程序的代码灵活易读并且易于维护。
一个最简单的特征模板的例子是定义在<limits>中的numeric_limits类模板。这个模板的基本定义如下(列举了几个常见的):
template<class T>
class numeric_limits
{
public:
static T min()throw();
static T max()throw();
static T epsilon()throw();
//Etc.
};
<limits>头文件为所有基本数字类型定义了特化。例如,若想得到浮点数字系统的double版本的基类型,可以使用表达式numeric_limits<double>::radix。为了得到有用的最小整数值,可以使用numeric_limits<int>::min()。
另外一个例子是char_traits类模板。Std::string类和std::wstring类是std::basic_string模板的特化,它的定义如下所示:
template<class charT,
class traits = char_traits<charT>,
class allocator = allocator<char T> >
class basic_string;
模板参数charT代表了基础字符类型,它通常是char类型或wchar_t类型。基本的char_traits模板是典型的空模板,标准库提供了对char和wchar_t进行的特化。下面是根据C++标准提供的一个char_traits<char>特化:
template<>
struct char_traits < char >
{
typedef char char_type;
typedef int int_type;
static int_type eof();
//Etc.
};
basic_string类模板使用这些函数,用于基于字符操作的通用的字符串处理。当声明一个string变量时,例如:
std::string s;
事实上,正在声明的s格式如下所示(由于在basic_string特化中有默认的模板参数):
std::basic_string<char, std::char_traits<char>,
std::allocator<char> > s;
由于字符特征已经从basic_string类模板中分离出来,可以使用一个惯用的特征来取代std::char_traits。
在《C++编程思想 第二卷》中在这里有一个更好的,更容易理解的一个特征模板例子,当然也更长,由于是总结,这里就不一一列出了。
特征类的使用提供了两个关键的优点:
1)在将对象与其关联的属性或函数配对方面提供了灵活性和可扩充性;
2)它保持了模板参数列表的短小易读。如果一个客人与30个类型相关,那么,将所有30个参数直接在每一个BearCorner声明中指定,这将是非常不方便的。而将这些类型放在一个独立的特征类中就会大大简化这项工作。
5.2 策略
将函数与模板参数关联起来也是有用的,因而客户端程序员在他们编码的时候能够轻松的定制代码行为,具体请查看《C++编程思想 第二卷》。
5.3 奇特的递归模板模式*
这一小节的内容不常用,可以作为选学内容,当然这里也就不再叙述了。
6、模板元编程
终于来到了将模板应用到极致的一个技术——模板元编程,能够耐心的看到这里,说明你的C++基础已经不错了,相信你会轻松地看完这节内容。
正如将进行类型参数代替作为一种方便的方法,这意味着产生一种支持编译时编程的机制。这样的程序称为模板元程序(template metaprogram),事实证明它可以做很多事。实际上,模板元编程支持选择(if-else)和循环(通过递归)。从理论上来讲,可以用它执行任何的计算。下面的例子显示了如何利用这个技术在编译时计算斐波那契(fibonacci)数:
#include <iostream>
using namespace std;
template<int n>
struct Fib
{
enum{ val = Fib<n - 1>::val + Fib<n - 2>::val };
};
template<>
struct Fib < 1 >
{
enum{ val = 1 };
};
template <>
struct Fib < 0 >
{
enum{ val = 0 };
};
void main()
{
cout << Fib<5>::val << endl; //5
cout << Fib<20>::val << endl; //6765
}
当编译器试图对
Fib<5>
进行实例化时,编译器发现必须先实例化
Fib<4>
,而后者要求实例化
Fib<3>
,依次类推,这个递归最终在
Fib<0>
时结束,此时计算展开,
Fib<5>::val
由常量
5
代替。
由于所有的计算都由编译器来做,其包含的值必须是编译时常量,因此使用了enum。
6.1 编译时循环
在一个模板源程序中要计算任意的循环,首先必须用公式表示递归。下面的例子计算整数n的p次方:
#include <iostream>
using namespace std;
template<int N, int P>
struct Power
{
enum{ val = N*Power<N, P - 1>::val };
};
template<int N>
struct Power < N, 0 >
{
enum{ val = 1 };
};
void main()
{
cout << Power<2, 5>::val << endl; //32
}
6.2 循环分解
算法设计者们总是尽力优化他们的程序。其中一个是在时间方面的优化——特别是在数字计算编程中——采用的是循环分解,这是一项将顶层循环的系数减小到最小的技术。请看下面计算整数幂的方法:
#include <iostream>
using namespace std;
template<int n>
inline int power(int m)
{
return power<n - 1>(m)*m;
}
template<>
inline int power<1>(int m)
{
return m;
}
template<>
inline int power<0>(int m)
{
return 1;
}
void main()
{
int m = 4;
cout << power<3>(m) << endl; //64
}
从概念上来讲,编译器必须生成模板参数分别为
3
、
2
、
1
的
3
个
power<>
特化。因为每个函数的代码可以内联,实际插入
main()
函数的代码就是一个单一的表达式
m*m*m
。这样一来,一个存在内联的简单模板特化就提供了一种方法,该方法可以完全避免循环控制顶层的出现。这种循环分解的方法受到使用的编译器的内联深度的限制。
6.3 编译时选择
为了模拟在编译时的条件,可以在一个enum声明中使用3目条件运算符。下面的程序就使用了这个技术,在编译时计算两个整数中的最大值:
#include <iostream>
using namespace std;
template<int n1, int n2>
struct Max
{
enum{ val = (n1 > n2) ? n1 : n2 };
};
void main()
{
cout << Max<10, 20>::val << endl; //20
}
6.4 编译时断言
断言基本上就是一个其后有一个适当动作的布尔表达式的判断:若条件为真则什么都不做,否则就停止并附带一个诊断信息。若可以在编译时对一个表达式求值,就可以使用断言。下面的例子使用了这个技术,它将一个布尔表达式映射到了一个数组声明中:
#define STATIC_ASSERT(x) \
do{typedef int a[(x) ? 1 : -1];} while (0);
void main()
{
STATIC_ASSERT(sizeof(int) <= sizeof(long));
STATIC_ASSERT(sizeof(double) <= sizeof(int));
}
定义一个大小为
-1
的数组是不合法的,因此当条件为假时这条语句将失败。
在效仿编译时断言方面剩下的问题就是打印一个有意义的错误消息并且停止编译。所有的编译错误都要求编译器停止编译;解决这个问题的一个技巧就是在错误信息中插入有用的文本。下面的例子使用了模板特化,一个局部类和一个小巧且奇妙的宏来完成这项工作:
#include <iostream>
using namespace std;
template<bool>
struct StaticCheck
{
StaticCheck(...);
};
template<>
struct StaticCheck < false > {};
#define STATIC_CHECK(expr, msg){ \
class Error_##msg{}; \
sizeof((StaticCheck<expr>(Error_##msg()))); \
}
template<class To, class From>
To safe_cast(From from)
{
STATIC_CHECK(sizeof(From) <= sizeof(To), NarrowingConversion);
return reinterpret_cast<To>(from);
}
void main()
{
void *p = 0;
int i = safe_cast<int>(p);
cout << "int cast okay" << endl;
char c = safe_cast<char>(p);
}
这个例子定义了一个函数模板
safe_cast<>()
用来进行两个对象长度的检查。他检查源对象类型长度是否不大于目标对象类型的长度。注意,
StaticCheck
类模板有一个奇特的特性:
任何模板参数的特化都可以被转换成StaticCheck<true>的实例(由于它的构造函数中的省略号),并且没有任何模板参数的特化都可以被转换成StaticCheck<false>的实例,因为没有为这种特化提供转换。
它的思想是:在编译时如果相关测试条件为真,就创建一个新类的实例并将它转换为StaticCheck<true>对象;或者当条件为假时,将它转换为一个StaticCheck<false>对象。这时编译器将作出解释:他不知道如何将这个新类类型转换成StaticCheck<false>对象。为了在错误消息中插入一些有用的信息,新的类名在它的名字中携带了关键且有意义的文字信息。
这里有一个标记传递预处理操作符:##,它将它的操作数连接为一个单一的标记,因此经过预处理后Error_##msg变成了标记Error_msg。
详细的解释请阅读《C++编程思想 第二卷》。
这里我想多说一些,在Boost库中有一个很有用的函数模板——checked_delete,代码如下:
template<class T>
inline void checked_delete(T * x)
{
// intentionally complex - simplification causes regressions
typedef char type_must_be_complete[ sizeof(T)? 1: -1 ];
(void) sizeof(type_must_be_complete);
delete x;
}
读者可能会问到这个模板是怎么工作的呢?sizeof(T)会等于0吗?其实问题不在这里,看看驱动程序就知道了:
#include <iostream>
using namespace std;
template<class T> inline void checked_delete(T * x)
{
// intentionally complex - simplification causes regressions
typedef char type_must_be_complete[sizeof(T) ? 1 : -1];
(void) sizeof(type_must_be_complete);
delete x;
}
template <class T>
void just_delete(T *ptr)
{
delete ptr;
}
struct SomeType1
{};
struct SomeType2;
void main()
{
SomeType1 *p1 = (SomeType1*)malloc(sizeof(SomeType1));
SomeType2 *p2 = (SomeType2*)malloc(0x123);
//just_delete(p1);// OK
//checked_delete(p1);// Ok
checked_delete(p2); // Error
//just_delete(p2);// Ok
}
我们声明了一个
SomeType2
结构体,但是未定义它,也就是他的类型不完整。但我们却可以将
malloc
出来的内存强制转换转换为
SomeType2*
,而当我们直接
delete p2
时编译器并不报错,想一想就觉得这是多么可怕,原因是
delete将执行两个操作:1、调用析构函数;2、释放内存。然而
SomeType2
类型仅仅只声明了而已,怎么会有析构函数呢?但是编译器确实通过了。
我们来看看check_delete函数模板发生了什么。checked_delete(p2)会实例化一个just_delete<SomeType2>()函数,然而由于SomeType2类型不完整,sizeof在编译期计算SomeType2类型大小时就出错了,这就是问题所在。
在编译器报错,比在运行期程序崩溃可能要好100倍。
6.5 表达式模板
这一节说实话我也不太懂,但是却是模板最强大的应用技术——表达式模板。我不清楚为什么要叫做表达式模板。但是书中例子还是看的懂的。
可能在日常编程中并不使用这种技术,因此我也不打算在在这节中叙述了,但是读者应该去研究研究书上的例子,还是很有趣的。C++有许多比较晦涩难懂的技术,说是技术,应该叫高级技巧才对,如果不是编写高效的库的话,还是尽量把你的代码让别人容易看懂为好,不要吃力却不讨好(作者观点)。
7、模板编译模型
从C语言转到C++的程序员应该有这样一个体会,我们都将完整类定义,模板定义放在头文件中。这种方法与传统的编程方法背道而驰,传统的编程方法通过将函数声明放在靠后的头文件中,而将函数实现放在独立的文件中的方法,使得普通函数的定义与他们的声明相分离。
与这种传统方法分离的理由如下:
1)头文件中的内联函数体会导致多函数的定义,从而导致链接错误。
2)隐藏来自客户有益函数的实现,从而减少了编译时连接。
3)商家可能将预编译代码(为一个特定的编译器编写)分配到各个头文件中,从而使得用户看不到函数的实现。
4)头文件越小,编译时间越短。
详细内容读者自行阅读那本老生常谈的书吧。
8、小结
很少在最后补上小结,但这次还是有必要的。
模板的广泛使用的程度远远超过简单地类型参数化。当对模板结合使用参数类型推断、用户自定义特化和模板元编程的时候,C++作为一种强有力的代码产生机制已经形成了。
这里有一个没有提及的C++模板的缺陷,就是在解释编译时的信息报告方面的困难。
读者从本节得到的另一个重要的思想就是,一个模板意味着一个接口。
另外,我们将会看到模板的一个最著名的应用:标准C++库的子集,即广为人知的标准模板库(STL),前面已经提及好多次了,如果有心的话,读者在前面都应该马上去看一看了。
(码这么多字确实不容易,不知你看懂了没?)