7.1 Template

原来Template被视为对container classes如Lists和Arrays的一项支持,但现在它已经成为标准模板库(STL)的基础。它也被用于属性混合(如内存策略)或互斥(mutual exclusion)机制的参数化技术之中。它甚至被使用与一项所谓的template metaprgrams:class expression templates将在编译时期而非执行期被评估,因为带来重大的效率提升。

但是template是最令程序员挫败的一个主题:错误消息可能远在真正问题的十万八千里就产生了,编译时间提高了不是,而程序极端的害怕修改一个内有多重相依关系的.H文件,程序大小会变得非常膨胀,而且template的所有行为超越了一般程序员的理解能力。

下面是关于template的三个主要讨论方向:

  1. template的声明。当你声明一个template class、template class member function等等发生什么。
  2. 如何“实例化(instantiates)”class object、inline nonmember 以及member template functions,这些是“每一个编译单元拥有一份实例”的东西。
  3. 如何“实例化(instantiates)”nonmember、member template functions以及static template class members,这些是“每一个可执行文件中只需要一份实例”的东西。

作者使用“实例化(instantiation)”这个字眼来表示“进程(process)将真正的类型和表达式绑定到template相关形式参数”操作;例如:

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 Instantiation)

考虑如下的template Point class:

template<class Type>
class Point
{
public:
	enum Status{unallocated,normalize};
	
	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 chuckSize;
	Type _x,_y,_z;
};

当编译器看到template class 声明时,实际程序中,什么反应读没有,也就是说static data members并不可用,nested enum或其他enumerators一样。

虽然eum Status的真正类型在所有的Point instantiations中都一样,其他enumerator也是,但它们每一个都只能通过template Point class的某个实例来存取或操作。因此:

//正确
Point<float>::Status s;
Point<float>::freeList;

//错误
Point::Status s;
Point::freeList;

如果如下这么写,将产生第二份实例,于Point class的double instantiation产生关联:

Point<double>::freeList;

当我们定义一个指针或者引用时候。它会实例化一个“Point的float的实例”(C++ Standard之前,指针并未被强制执行,编译器可以自行决定要不要将template实例化)。

const Point<float> &ref = 0;

//内部扩张
Point<float> temporary(float(0));
const Point<float> &ref = temporary;

因为reference并不是无物(no object)的代名词。0被视为整数,必须被转换为Point<float>类型的对象,如果没能转换的可能,这个定义就是错误的,会被编译器挑出来。

所以一个class object的定义,不论是由编译器做或是由程序员像如下显示的做,都会导致template class的实例化:

const Point<float> origin;

Point有三个nonstatic members,每一个类型都是Type,Type被绑定float,所以origin的配置空间足够容纳三个float成员。

然而,member functions(至少对那些未被使用过的)不应该被“实例化”。只有在member functions被使用的时,C++ Standard才要求它们被“实例化”。目前编译器并不精确遵循这项要求,而是由使用者来主导“实例化”规则,有两个原因:

  1. 空间和时间效率的考虑。如果class中有100个member functions,但你只使用了某个类型的其中两个,针对另一个类型的五个,那么其他的193个函数都实例化将会浪费大量的时间和空间。
  2. 尚未实现的机能。并不是一个template实例化的所有类型就一定能够完整支持一组member functions所需要的所有运算符。如果只实例化真正用到的member functions,template就能够支持那些原本可能会造成编译时期错误的类型。

当程序员写下面的代码时候,只需要调用Point的default constructor和destructor,只有这两个函数被实例化:

Point<float>* p = new Point<float>;

只有Point template的float实例、new运算符和default constructor需要被实例化。

这些函数在什么时候实例化,目前流行两种策略:

  1. 在编译的时候,那么函数将实例化于origin和p存在的那个文件中。
  2. 在链接的时候。那么编译器会被一些辅助工具重新激活。template函数实例可能被放在这一文件中、别的文件中或一个分离的存储位置。

在int和long一致(或double和long double一致)的架构中,两个类型实例化操作,目前所有编译器都会产生两个实例。C++ Standard并未对此作出强制规定。

Template的错误报告(Error Reporting within a Template)

考虑如下的声明:

template<class T>
class Mumble
{
public$:
	Mumble(T t = 1024)
		:_t(t)
	{
		if(tt != t)
			throw ex ex;
	}
private:
	T tt;
}

这个Mumble template class声明内含一些即露骨由潜沉的错误:

1,public旁多了一个$;2,t被默认初始化为1024,这个要视T的真正类型而定;3,_t并不是一个member的名称,使用tt才行;4,使用!=运算符也是要视T的真正类型而定;5,意外的输入了ex两次;6,忘记以一个分号作为class声明的结束。

在一个nontemplate class声明中,这6个错误会被编译器挑出;但template class却不同,所有与类型有关的检验,如果涉及到template参数,必须延迟到真正的实例化操作发生,才会开始。

词汇分析器(lexical analyzer)会在捕捉到$不合法的字符;解析器(parser)不会对未命名的_t视为错误,但是为抓住ex出现两次和缺少一个分号错误。

在一个十分普遍的替代策略中,template的声明被视为一些列的lexical tokens,而parsing操作延迟直到真正有实例化操作发生时才开始。每当看到一个实例化的时候,这组token就会被推往parser,然后调用类型检查,等等。

目前的编译器,面对一个template声明,在它被一组实际参数实例化之前,只能施行于有限的错误检查。template中那些与语法无关的错误,程序员可能认为十分明显了,编译器却让它通过,只有在特定实例被定义之后,才会发出抱怨。

Nonmember和member template functions在实例化行为发生之前也一样没做完全的类型检查。

Template中的名称决议法(Name Resolution within a Template)

你必须能区分两种意义:一种是C+ Standard所谓的“scope of the template definition”,也就是“定义出template”程序端。另一个是C++ Standard所谓的“”scope of the template instantiation,也就是“实例化template”的程序端:

//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(),如下操作是调用哪个foo() : 

sr0.invariant();

结果是直觉以外的一个:

//scope of the template definition
extern double foo(double);

Template中,对于一个nonmember name的决议结果是:根据这个name使用是否与“用以实例化该template的参数类型决定的”,如果其使用互不相关,那么就以“scope of the template declaration”来决定name,如果使用互有关联,那么久以“scope of the template instantiation”来决定name。

例子中,foo()于用以实例化ScopeRules的参数类型无关:

_member = foo(_val);

如下是另一种情况,“与类型相关”(type-)dependent的用法:

sr0.type_dependent():

//内容如下
return foo(_member);

这个例子很清楚于template参数有关,所以这次foo()必须是“scope of the template instantiation”那个。

这意味着一个编译器必须保持两个scope contexts:

  1. “scope of the template declaration”,用以专注于一般的template class。
  2. “scope of the template instantiation”,用以专注于特定的实例。

编译器的决议(resolution)算法必须决定哪一个才是适当的scope,然后在其中搜寻适当的name。

Member Function的实例化行为(Member Function instantiation)

对于template的支持,最困难的莫过于template function的实例化。目前的编译器提供了两种策略:一个是编译时期策略,程序代码必须在program text file中备妥可用;另一个是链接时期策略,有一些meta-compilation工具可以引导编译器实例化行为。

目前,不论是编译期还是链接时期的实例化策略,均存在如下弱点:当template实例被产生出来时,有时候会大量增加编译时间。很显然,这是将template functions第一次实例化时的必要条件。然而当那些函数被非必要地再次实例化,或是当“决定那些函数是否需要再实例化”所花的代价太大时,编译器的表现令人失望。

C++ Standard现在已经扩充了对template的支持,允许程序员显示的要求在一个文件中将整个class template实例化,或是针对一个template class的个别member function:

template class Point3d<float>;

//template 的 member function
template float Point3d<float>::X() const;

//template function
template Point3d<float> operator+
			(const Point3d<float>&,const Point3d<float>&);

实际上,template instantiation似乎拒绝全面自动化,甚至虽然每一件工作都做对了,产生出来的object files的重新编译成本仍然可能太高——如果程序十分巨大的话。以手动方式现在个别的object module中完成预先实例化操作(pre-instantiation),虽然沉闷,却是唯一有效的方法。

猜你喜欢

转载自blog.csdn.net/weixin_28712713/article/details/84934348
7.1