Template instantiation analysis

On_Demand实例化

 

对于一个类X,在哪些情况下必须要求看到X的定义:

(对于怎样判断一个行为是声明还是定义,已在我的这篇文章中有所论述:《将模板声明头文件和模板定义源文件分离》)

1,创建X类型的对象时:不论这种创建是直接的还是间接的(例如X类型对象作为另一个类的成员被创建),都必须看到X的定义,因为创建一个对象,就要知道类型的内存布局,而没有定义,怎么会知道内存布局呢?对于直接创建:例如X x=new X();这其中涉及有:是否X提供了默认构造函数、是否重载了operator new操作符,是否operator new操作符被设置为非public的(有些类类型不允许用户在外界创建这种类型的对象,而只能使用成员方法创建对象——可以用来限制用户只允许创建固定数量的对象Scott Meyers的Effective C++有详细论述)、、、因此必须访问到X的定义


2,访问X的内部成员变量的声明(因为一般非静态对的成员变量都被看做是定义,所以这里指的是静态的成员变量):这种情况毋庸置疑必须看到定义


3,需要X类型的大小的时候: 原因相同,需要X的内存布局以得出大小,因此必须看到定义


4,将X类型的对象转型为其他类型(或者相反):因为需要访问内存大小和布局,所以必须看到定义


5, 将X类型的指针、引用转型为其他表达式(或者相反):需要看到定义


6, 定义和调用返回值类型、形参类型为X的函数:需要看到定义(只是声明则不需要)——原因解析:

#include
using namespace std;
class X
{
 X(int);
};
void FUN(X x)
{}
void main()
{
 int a = 10;
 FUN(a);
}


当FUN(a)调用的时候编译器要看FUN函数的参数类型X是否提供了可以从int向X转型的单参构造函数,所以这里自然需要访问X的定义

重载情况下的分析:

#include
using namespace std;
class X
{
 X(int);
};
void FUN(X x)
{}
void FUN(double a)
{}
void main()
{
 int a = 10;
 FUN(a);
}

当FUN(a)调用的时候,可以隐式转型为X对象,也可以向上转型为double,此时就需要访问X是否定义了这种转型构造函数来达成最精确的匹配

 

不需要X的定义的情况:当只用到X的类型的时候,例如X * x;X & x;此时不会要求X的定义——这也是为什么这种情况可以通过编译:

class X
{
 //可以通过编译
 X * x;
 X & xx;
 //X x = new X();//error:此时X内存布局未知
};

有了上述的介绍之后,其实所谓的On_Demand实例化很简单:根据需要才产生实例化体

例如:X * x=0;这里不需要看到实例化体X的定义,因此不需要进行实例化!

但是对于需要访问X类定义的时候——X::static_member,必须看到X的定义,此时将执行实例化!

 

 


延迟实例化


什么是延迟实例化: 在隐式实例化类模板的时候,只对确实需要的部分才进行实例化;

 

1,隐式实例化时而不是显式实例化时

class X
{
public:
 void FUN()
 {
  char arr[0];//错误发生地
 }
};
template class X;//显式实例化
void main()
{
 X x;//隐式实例化
}


这就有意思了!当显式实例化的时候char arr[0];会报告错误,但是隐式实例化的时候char arr[0];却没有报告错误!——这里并没有使用到FUN成员函数,显式实例化却对FUN进行了实例化(隐式实例化没有对FUN进行实例化),由此可以得出延迟实例化只对隐式实例化使用,对显式实例化不适用(显式实例化将实例化所有的实体)


2,确实需要实例化的部分:

1)类模板的所有成员声明(不包括函数声明 ,这和C++templates延迟实例化那里所讲的一定会实例化所有成员声明有出入,后面示例中会验证

2)如果类内部有匿名的union,会实例化该union内部定义的所有成员

3)虚函数的定义一般会被实例化:实例化虚函数定义的原因是“实现虚函数调用机制的内部结构”要虚函数作为链接实体而存在

4)缺省的函数调用实参( 视情况 ):当这个缺省调用实参确实被用到了(用户没有提供实参取代这个缺省实参),那么才会实例化该缺省实参(否则不实例化)

 

一个综合的例子:


#include
using namespace std;

template
class Safe
{
};

template
class Danger
{
public:
	//验证假定模板实参处于最理想的状态
	typedef char Block[N];
};

template
class Tricy
{
public:
	virtual ~Tricy(){}

	//验证所有的声明一定会被编译——static类型变量danger的创建是声明
	//static Danger danger;

	//这里可以验证不管是函数声明还是函数定义,只有此函数确实被使用到的时候才会对此函数进行编译
	Danger  no_body_here(Danger no_boom_yet);

	//验证假定缺省实参不会被使用
	void inclass(Safe =3) {
		Danger no_boom_yet;
	}

	//验证虚函数的定义一定会被实例化
	//virtual Safe suspect();

	//验证没有没使用到的成员不会被实例化
	struct Nested {
		Danger pf;
	};

	//验证匿名unio中定义的成员一定会被实例化
	union {
		int align;
		//Danger anonymous;
	};

};
void main()
{
	//Tricy test;

	//使用了成员函数的时候成员函数才会被实例化
	//test.no_body_here(Danger<-1>());
}

上面的代码说明了编译器在编译模板时的两点策略:
1,假定模板实参处于最理想的状态——对于特定的模板实参可能会出现的语义错误,编译器假定模板实参不会取这些特定的值,也就是假定模板实参最理想
2,假定缺省实参不会被使用——对于该函数缺省实参可能会导致的语义错误,编译器假定该缺省实参不会被用到

 

两阶段查找(two-phase lookup)


 我们先认识一下编译器对于一个名称的以下两点查找策略:
 1,当编译器看到一个(受限的)依赖型的模板名称的时候,并不对其查找并解析,而是要等到实例化的时候再在实例化点进行查找并解析!

(因为依赖嘛,依赖于所依赖的模板被实例化之后才进行查找!)

2,对于非依赖型的名称,编译器将在看到它们的“第一眼”就进行查找(因为不依赖)(但是如果是涉及到基类中的名称,将不会在依赖型的基类中查找非依赖型的名称,详情在我的文章《模板中的名称》中有详细分析)

 

一个示例说明问题:

#include
using namespace std;

template
class Trap{	
public:
	enum{	
		x//这里x是一个值	
	};
};

template
class Victim{
public:	
	static int y;
		static void poof()
		{		
			//问题就出在这里	
			int c=Trap::x * y;	
		}
};

//特化
template<>
class Trap{
};

void main()
{
	//当用double实例化的时候,Trap类中确实有一个可以支持int c=Trap::x * y;	运算的x成员变量
	Victim::poof();

	//但是当用int实例化的时候,Trap中没有可以支持int c=Trap::x * y;	运算的x成员变量
	//Victim::poof();
}
		

当编译器编译 int c=Trap::x * y;    这句话时,编译器看到Trap::x 是个(受限的)依赖型的名称,那么不会对其查找(其实这里Trap::aaaaa*y;也可以通过编译,原理还是一样——视其所依赖的模板被实例化之后进行查找),但是当编译器看到

Victim::poof();的时候,int c=Trap::x * y;变成了int c=Trap::x * y;这里x的依赖性已经消失,而是明确指定x将在Trap类型的定义中进行查找,于是当编译器发现Trap的定义中(对其定义进行了特化)没有x成员的时候,将对其报错!(对于这种看到依赖型名称为什么不对其查找的原因,在我的文章《模板中的名称》中有所论述,简单而言就是将导致二义性)

 

相信根据上面的分析,两阶段查找的意义已经跃然水面了!

 

这里给出两阶段查找时在每个阶段所做的事情:

第一阶段:发生在还没有实例化的时候,对非依赖型名称进行查找,另外,对于非受限的依赖型(int c=Trap::x * y;中的x是个受限的依赖型名称)名称(例如某个函数,函数的参数中有依赖型名称),将进行不完整的查找!

第二阶段:发生在实例化的时候,对依赖型受限名称进行查找,并且对于非受限的依赖型名称(例如某个函数,函数的参数中有依赖型名称)使用ADL再次进行查找!(对于ADL,在文章《模板中的名称》中有所论述

 

POI(point of instantiation 实例化点)

藉由上面的分析,即编译器在客户端代码的实例化动作发生的时候将进行第二次对(受限的)依赖型名称的查找,此时将在模板实例化的定义中查找名称,由此可以看出此时编译器必须看到该模板实例化的定义,于是当客户端代码发生了实例化一个模板的动作的时候,将会在源代码中产生一个实例化点(POI),这个点就是实例化代码的定义安置的地方!

 

 

实例化点在哪里:

1,对于函数模板而言:

#include
using namespace std;
class Myint {
public:
	Myint(int i);
};

Myint operator - (Myint const &);

bool operator>(Myint const &, Myint const &);

typedef Myint Int;

template
void f(T i) {
	if (i > 0) {
		g(-i);
	}
}

//实例化点3
void g(Int) {
	//实例化点1
	f(42);
	//实例化点2
}
//实例化点4

void main() {
}

对于f(42)的调用,编译器将会实例化出一个f模板的实例化体,然后再使用指向该实例化体的引用来调用此函数,因此这里将实例化出一个void f ()函数的实例,但是void f ()函数定义的位置应该在哪里呢?代码中标出了四个可能安置void f ()函数定义的位置,虽然1,2实例化点是离调用点近的地方,但是C++不会将void f ()函数定义安插在那里,以下分析实例化点3,4;

Template instantiation analysis_第1张图片

从图中可以看出,如果将void f ()函数定义的位置安插在实例化点3,那么将导致调用g函数的时候g函数不可见,但是如果插入到实例化点4,调用g函数的时候g函数将可见!

其结果是void f ()函数的定义会被安插在实例化点4——规则为:对于指向非类型特化的引用,C++会把它的POI定义在“包含这个引用的定义或声明的最近名字空间作用域”中

 

2,对于类模板而言:

#include
using namespace std;

template
class S
{
public:
	T a ;
};

//3
int H()
{
	//1
	return sizeof(S);
	//2
}
//4

void main()
{
	cout << H() << endl;//输出结果为8
}

对于上面的示例,我们知道1、2位置不能作为POI(模板不能出现在函数内部),排除1、2,如果我们按照非类型POI的规则(即上面函数POI的规则),那么S的定义将在4处,然而如果这样,sizeof(S)将只能等到4处编译完了才能知道结果,因此编译器必须在return sizeof(S);的时候看到S的定义,于是S实例化点为3处!

 

小结一下以上函数模板、类模板的POI规则:对于函数模板,因为涉及到标识符可见不可见的问题,所以编译器将POI安置在了实例化动作的后面,对于类模板,因为涉及到对实例化类定义的访问,所以编译器将POI安置在了实例化动作的前面!

 

当函数模板的POI中嵌套类模板的POI(多层POI):

示例:

#include
using namespace std;

template
class S
{
public:
	typedef int I;
};

template
void f()
{
	S::I var1 = 41;
	typename S::I var2 = 42;
}
void main()
{
	f();
}
//(2):(2a),(2b)

Template instantiation analysis_第2张图片

如图,我们易知S的POI,这里关键的是S的POI:f的POI在2处,当进行f的实例化时才进行S的实例化,但是此时S的POI规则不再按照类模板的POI规则了,这时的规则是:对于非类型实体,二次POI的位置和主POI(f)的位置相同,对于类型实体,二次POI的位置在主POI(f)的位置紧前处!于是我们可知S的POI在f的紧前处!

 

当一个翻译单元包含同个实例的多个POI:

1,对于类模板实例而言:只有首个POI会被保留,其他的POI会被忽略(不会将其后的POI看做POI)

2,对于非类型实例:所有的POI都会保留

(大多数编译器会延迟非内联函数的实例化,知道翻译单元末尾才进行实例化)

 

当两阶段查找遇见分离模型——导致二义性

简单而言,这种二义性是由于在进行模板解析第一阶段时绑定的函数和进行第二阶段将模板函数实例化之后生成POI之后又进行绑定的函数因为无法重载而导致了二义性!(因为C++11舍弃了export关键字的原因,无法对其进行演示,关于export导出模板,可在我的文章《将模板声明头文件和模板定义源文件分离 》中了解到!)

 

 

几种实现方案

抛砖引玉:

C++编译器的两个基本组件介绍:编译器、链接器

编译器:将源代码翻译成目标文件,目标文件包含机器代码,有些机器代码则具有符号注解(用于跨文件引用其他目标文件或者程序库)

链接器:组合目标文件,解析目标文件中包含的符号注解(用于跨文件引用其他目标文件或者程序库),最后生成可执行的程序或者程序库

 

1,多个翻译单元中重复类模板的实例化过程:因为类定义不会直接产生低层次的代码,并且C++也只是在内部使用这些类定义,所以这基本不会有问题——因此在多个翻译单元中重复包含同一个类定义不会有问题!

 

2,多个翻译单元中重复包含函数定义(非inline):

——多个翻译单元中只能有一份函数的定义!(对于普通的函数来说

 

对于函数模板这种情况需要分析:

一个示例:

头文件a.h

//a.h
#ifndef AH
#define AH

template
class S
{
public:
	void f();
};

//这里仅仅是成员函数模板的定义
template
void S::f()
{
}

void helper(S * );

#endif

源文件a.cpp

//a.cpp
#include"a.h"
void helper(S * s)
{
	//这里是S::f的一个POI
	s->f();
}

dot-C文件main.cpp

#include
#include"a.h"
using namespace std;

void main()
{
	S s;

	//调用a.cpp文件中定义的函数helper
	//helper(&s);作用——执行helper函数定义,让helper函数定义的内部实例化S::f函数
	helper(&s);

	//这里是S::f的一个POI
	s.f();
}

对于上面这个示例,有几点需要事先了解的:

1,对于s->f();如果s是一个依赖型名称,那么f也将是一个依赖型名称

2,已知s->f();中f是一个依赖型名称,这里还有一个延迟实例化的问题,即虽然f是依赖S模板的模板参数来实例化的,但是S实例化的时候f不会马上实例化,而是在实例化之后S调用f的时候才执行f的实例化!因此:

void helper(S * s)
{
	s->f();
}

中,S * s是实例化了S了,但是f的实例化却不是在这里实例化,而是在调用f的地方    s->f();  实例化的!因为此时需要f!

(对于这一条中的内容:为什么f是依赖型名称在我的文章《模板中的名称》中有详解!对于为什么f是在  s->f();  处才实例化的,则是上面所讲的延迟实例化!)

3,编译器对待普通函数的多次定义的情况——根本不允许普通函数多次定义(但是C++模板却不遵守这个普通的规则

4,对于为什么要将模板的声明和模板的定义放在同一文件夹下(包含模型),在我的文章《将模板声明头文件和模板定义源文件分离 》中有详细的解析!

 

知道了以上知识点之后,我们知道上面的示例中有两处需要实例化S::f的实例化点,面对这个问题,如果正如上面说的,C++模板不遵守普通的“不允许函数多次定义”的规则,那么C++取而代之的规则又是什么呢?(即C++允许函数模板多次定义,但是又不违反定义只能有一份有效!

 

以下是三种解决上述问题的方案:

1,贪婪实例化

机制:贪婪实例化假定链接器知道:特定的实体可以在多个文件中多次出现,于是,编译器会使用某种方法对这些实体进行标记,当链接器找到多个实体的时候,它会保留其中一个而抛弃所有的其他实例

 

缺点:

1)为无谓的生成模板实例和优化模板实例浪费时间,可是最后只有一个实例化保留下来!

2)链接器通常不会检查两个实例化体是否一样;

3)与其他解决方案比较,所有目标文件的大小总和可能更大(相同的代码会生成很多次!)

 

但这些缺点看起来并不是很严重,因为与其他方案比起来,这种方案有一个很大的优势:保留了源对象之间的原始依赖性!

2,询问实例化

机制:这种方案维护一个数据库,数据库中储存着那些已经实例化完成了的实例化体,和实例化该实例化体需要依赖于哪些源码等信息;当遇到一个POI时,根据具体的前提,将从下面的三种操作中选择一种:

1)不存在所需的实例化体:进行实例化,将生成的实例化体放进数据库!

2)所需的实例化体已经存在,但是过期:进行新的实例化,并用新的实例化体替换数据库中旧的实例化体!

3)有所需的实例化体,并且没过期:不进行实例化!

 

这种方案带来的挑战:

1)需要根据源代码的状态,正确地维护数据库内容之间的依赖性!

2)并行编译多个源文件是很正常的事,因此,要获得具有工业强度的实现,就需要在数据库中提供相应的并行控制!

3)另外一个询问实例化的缺点概括而言则是管理这个数据库很难!

(如果忽略这些挑战,将可以高效地实现这个方案)

3,迭代实例化

机制:

1)编译器将源代码编译为目标文件(不进行任何的实例化

2)链接器解析目标文件错误(是否缺少实例化体

3)编译器从源文件中生成缺少的实例化体(补充目标文件

 

缺点:

1)要完成一次完整的链接,所需的时间不仅包括预链接器的时间开销,还包括每次询问重新编译和重新链接的时间

2)把诊断信息延迟到了链接期

3)需要进行特别处理,来记住包含特殊定义的源代码的位置——使用一个中心库(不得不克服询问实例化方案针对中心数据库的一些挑战!)

 

 


显式实例化

显式生成POI可以将模板实例化体的位置统一管理(不至于随处都生成模板的定义),并且,自动实例化会对创建时间产生严重的负面影响,所以手动实例化将提高创建效率!

4种有效的显式实例化函数模板的方法:

#include
using namespace std;

template
void f(T)throw(T)
{
}

template void f(int)throw(int);
template void f<>(float)throw(float);
template void f(long)throw(long);
template void f(char);

void main()
{

}

有两点需要注意:

1)模板实参可以通过演绎获得

2)可以不用写异常规范(如果要写,就要匹配相应的模板)

 

显式实例化与类:

例子说明一切:

#include
using namespace std;

template
class X
{
	void f()
	{
	}
};

//实例化类模板的成员
template void X::f();

//验证特化之后还能显式实例化吗
template<>
class X
{
	void f()
	{
	}
};

//实例化整个类模板——相应的所有成员都将被显式实例化
template class X;

//可以多次显式实例化,但是这里的显式实例化操作不会有任何影响
template class X;
void main()
{
}

注意:当显式实例化之后,不能再对模板进行对应的特化(但是反过来是可以的,即先全局特化一个模板,然后提供一个模板的显式实例化,因为全局特化不会产生实例化体,而显式实例化时提供一个这种特化的一个实例(POI)两者不矛盾,可以在《Specialization and Overload (特化与重载)》中查看详细信息!)

 

 

你可能感兴趣的:(Template instantiation analysis)