template是最令程序员挫败的一个主题,这一节的聚焦放在template的语意上面,我们将讨论templates在编译系统中“何时”、“为什么”以及“如何”发挥其功能。下面是有关template的三个主要讨论方向:
- template的声明。基本来说是当你声明一个template class、template class member function等等时,会发生什么事情。
- 如何“实例化”class object、inline nonmember以及member template functions。这些是“每一个编译单位都会拥有的一份实例”的东西。
- 如何“实例化”nonmember、member template functions以及static templates class members。这些都是“每一个可以执行文件中只需要一份实例”的东西。
我们使用“实例化”来表示进程(process)将真正的类型和表达式绑定到template相关形式参数(formal parameters)上头。
举个例子。
template <class Type>
Type
min(const Type &t1, const Type &t2);
用法如下:
min( 1.0, 2.0);
于是进程就把Type绑定为double并产生min()的一个程序文字实例(并施加以“mangling”手术,给它一个独一无二的名字),其中t1和t2的类型都是double。
Template的“实例化”行为
考虑下面的template Point class:
template<class Type>
class Point
{
public:
enum Status{unallocated, normalized};
Point(Type x = 0.0, Type y = 0.0,Type z=0.0);
~Point();
void *operator new(size_t);
void operator delete(void*, size_t);
//...
private:
static Point<Type> *freeList;
static int chunkSize;
Type _x, _y, _z;
};
首先,当编译器看到template class声明时,它会做出什么反应?在实际的程序中,什么反应也没有。也就是说,上述的static data members并不可用。
虽然enum Status的真正类型在所有的Point instantiations中都一样,其enumetators也是,但是他们每一个都只能够通过template Point class的某个实例来存取。因此我们可以这样写:
// ok
Point<float>::Status s;
但是不能这样写:
// error
Point::Status s;
即使两种类型抽象也是一样的。同样的道理,freeList和chuckSize对程序而言也还不可用,我们不能够写:
// error
Point::freeList
我们必须显示地指定类型,才能够使用freeList:
//ok
Point<float>::freeList
像上面这样使用static member,会使其一份实例与Point class的float instantiation在程序中产生关联。如果我们写:
//ok:另一个实例
Point<double>::freeList
就会出现第二个freeList实例,与Point class的double instantiation产生关联。
如果我们定义一个指针,指定特定的实例,像这样子:
Point<float> *ptr = 0;
再一次,程序什么都没有发生。为什么?因为一个指向class object的指针,本身并不是一个class object,编译器不需要知道与该class有关的任何members的数据或object布局数据。
如果不是pointer而是reference,又如何?假设:
const Point<float> &ref = 0;
是的,它真的会实例化一个“Point的float实例”。这个定义的真正语意会被扩展为:
//内部扩展
Point<float> temporary( float(0) );
const Point<float> &ref = temporary;
为什么?因为reference并不是无物(no object)的代名词。0被视为整数,必须被转换成以下类型的一个对象:
Point<float>
如果没有转换的可能,这个定义就是错误的,会在编译时被挑出来。
所以,一个class object的定义都会导致template class 的“实例化”,也就是说,float instantiation的真正对象布局会被产生出来。回顾先前的template声明,我们看到Point有三个nonstatic members,每一个的类型都是Type。Type现在被绑定为float,所以origin的配置空间必须足够容纳三个float成员。
这些函数在什么时候“实例化”?目前流行两种策略:
在编译的时候。那么函数将“实例化”于origin和p存在的那个文件中。
在链接的时候。那么编译器会被一些辅助工具重新激活。Template函数实例可能被放在这一个文件中、别的文件中或者一个分离的存储位置。
Template的错误报告(Error Reporting within a Template)
考虑下面的template声明:
(1)template <class T>
(2)class Mumble
(3){
(4)public:
(5) Mumble(T t=1024)
(6) :_t(t)
(7) {
(8) if(tt != t)
(9) throw ex ex;
(10) }
(11)private:
(12) T tt;
(13)};
这个Mumble template class的声明内含有一些错误:
L5:t被初始化为整数常量1024,或许可以。视T的真实类型而定,只有template的各个实例才能诊断出来。
L6:_t并不是哪一个member的名称,tt才是。这种错误一般会在“类型检验”这个阶段被找出来。
L8: !=运算符可能已经定义好了,也可能还没有定义,视T的真正类型而定,和L5相同。
L9:我们意外的键入ex两次。
L5和L8的潜在错误会在每个实例化操作(instantiation)发生时被检查出来并记录之。
cfront对template的处理是完全解析(parse)但不做类型检查;只有在每一个实例化发生时才做类型检查。
Template中名称决议法
一种是C++ Standard所谓的“scope of the template definition”,也就是“定义出template”的程序端。另一种是C++ Standard所谓的“scope of the template instantiation”,也就是“实例化”的程序端。
第一种情况如下:
//scope of the template definition
extern double foo(double);
template <class type>
class ScopeRules
{
public:
void invariant(){
_member = foo( _val );
}
type type_dependent()
{
return foo(_member);
}
//...
private:
int _val;
type _member;
};
第二种情况如下:
//scope of the template instantiation
extern int foo(int);
//...
ScopeRules<int> sr0;
在ScopeRules template中有两个foo()调用操作。在“scope of the template definition”中,只有一个foo()函数声明在scope之内。然而在“scope of the template instantiation”中,两个foo()函数声明都位于scope之内。
//scope of the template instantiation
sr0.invariant();
那么,在invariant()中调用究竟是哪一个foo()函数实例呢?
_member = foo(_val);
在调用foo()上,这里有俩个函数实例:
//scope of the template definition
extern double foo(double);
//scope of the template instantiation
extern int foo(int);
在Template之中,对nonmember name的决议结果,是根据这个name的使用是否与“实例化该template的参数类型”有关决定的。如果其使用互不关联,那么就以“scope of the template definition”来决定name。如果其使用相互关联,那么就以“scope of the template instantiation”来决定name。
在_member = foo(_val);中,_val与该template的参数类型无关,它是int固定类型。
//the resolution of foo() is not
//dependent on the template argument
_member = foo( _val );
因此选择
//scope of the template definition
extern double foo(double);
让我们看看“与类型相关”(type-dependent)的用法:
sr0.type_dependent();
这个函数的内容如下:
return foo(_member);
它会调用哪一个foo()呢?
这个例子中,很清楚的看到与template参数有关,所以这一次foo()必须在“scope of the template instantiation”中决议。由于_member实例化的类型是int,因此int版本的foo()出线。
Member Function的实例化行为
对于template的支持,最困难的莫过于template function的实例化(instantiation)。目前编译器提供两种策略:一种是编译时期策略,程序代码必须在program text file中准备妥了。另一种是链接时期策略,有一些meta-compilation工具可以引导编译器的实例化行为。