[C++ Template]深入模板--实例化

目录

第10章 实例化

10.1 On-Demand实例化

10.2 延迟实例化

10.3 C++的实例化模型

10.3.1 两阶段查找

10.3.2 POI

10.3.5 例子

10.5 显式实例化


第10章 实例化

模板实例化是一个过程, 它根据泛型的模板定义, 生成(具体的) 类型或者函数。 在C++中, 模板实例化是一个很基础的概念, 但却多少有一些错缩复杂。 复杂性的一个主要原因在于: 对于产生自模板的实体(指具体类型或函数) , 它们的定义已经不再局限于源代码中的单一位置。 事实上, 模板本身的位置、 使用模板的位置、 定义模板实参的位置都会对这个(产生自模板的) 实体的含义产生一定的影响。

 

10.1 On-Demand实例化

当C++编译器遇到模板特化的使用时, 它会利用所给的实参替换对应的模板参数, 从而产生该模板的特化。 这个过程是编译器自动进行的, 并不需要客户端代码来引导(或者不需要模板定义来引导) 。 而且, on-demand实例化的这个特性也使得C++模板和其他编译型语言的相似功能大有区别。 另外, on-demand实例化有时也被称为隐式实例化或者自动实例化

on-demand实例化表明: 在使用模板(特化) 的地方, 编译器通常需要访问模板和某些模板成员的整个定义(也就是说, 只有声明是不够的) 。 考虑下面这个包含短小源代码的文件:

template<typename T> 
class C;//(1)这里只有声明

C<int>* p = 0; //(2)正确:并不需要C<int>的定义

template<typename T>
class C 
{
    public:
    void f(); //(3) 成员声明
}; //(4) 类模板定义结束

void g (C<int>& c) //(5) 只使用类模板声明
{
    c.f(); //(6) 使用了类模板的定义
} // 需要C::f()的定义

在源代码的(1) 处, 只有模板声明是可见的, 也就是说: 模板定义此时还不是可见的(这类声明有时也被称为前置声明) 。 与普通类的情况一样, 如果你声明的是一个指向某种类型的指针或者引用(如(2) 处的声明) , 那么在声明的作用域中, 你并不需要看到该类模板的定义。例如, 声明函数g的参数类型并不需要模板C的完整定义。 然而, 如果(某个组件) 期望知道模板特化的大小, 或者访问该特化的成员, 那么整个类模板的定义就需要位于作用域中; 这也是源代码的(6) 处需要模板定义的原因。 因为如果看不见这个模板定义的话, 编译器就不能确定成员f存在且是可访问的(就是说, 不是私有的, 也不
是受保护的) 。

下面是另一个需要进行(前面的) 类模板实例化的表达式, 因为编译器需要知道C<void>的大小:

C<void>* p = new C<void>;

在这个例子中, 实例化是必不可少的, 因为只有进行实例化之后,编译器才能知道C<void>的大小。 对于上面这个特殊的模板, 你可能会认为: 用任何类型的实参 X 替换参数T之后, 都不会影响模板(特化)的大小; 因为在任何情况下, C<X>都是一个空类。 然而, 编译器并不会检测它是否为空。 而且, 为了确定 C<void>是否具有可访问的缺省构造函数, 并且确认C<void>没有声明私有的operator new 或者operator delete, 我们需要进行实例化。

 

10.2 延迟实例化

现在就有了一个相关的问题: 模板的实例化程度是怎么样的呢? 对于这个问题, 一个模糊的回答会是: 只对确实需要的部分进行实例化。换句话说, 编译器会延迟模板的实例化。 让我们细究“延迟”在这里的具体含义。

当隐式实例化类模板时, 同时也实例化了该模板的每个成员声明,但并没有实例化相应的定义。 然而, 存在一些例外的情况: 首先, 如果类模板包含了一个匿名的 union, 那么该union 定义的成员同时也被实例化了。 另一种例外情况发生在虚函数身上: 作为实例化类模板的结果, 虚函数的定义可能被实例化了, 但也可能还没有被实例化, 这要依赖于具体的实现。 实际上, 许多实现都会实例化(虚函数) 这个定义,因为“实现虚函数调用机制的内部结构”要求虚函数(的定义) 作为链接实体存在。

当实例化模板的时候, 缺省的函数调用实参是分开考虑的。 准确而言, 只有这个被调用的函数(或成员函数) 确实使用了缺省实参, 才会实例化该实参。 就是说, 如果这个函数(或成员函数) 不使用缺省调用实参, 而是使用显式实参来进行调用, 那么就不会实例化缺省实参。对于上面的这些规则, 让我们用下面的例子来阐述:

template <typename T>
class Safe {
};

template <int N>
class Danger {
public:
	typedef char Block[N]; // 如果N<=0的话, 将会出错
};

template <typename T, int N>
class Tricky 
{
public:
	virtual ~Tricky() {
	} 
	void no_body_here(Safe<T> = 3);
	void inclass() {
		Danger<N> no_boom_yet;
	} //
	void error() { Danger<0> boom; }
	// void unsafe(T (*p)[N]);
	T operator->();
	// virtual Safe<T> suspect();
	struct Nested {
		Danger<N> pfew;
	};
	union { // 匿名的union
		int align; Safe<T> anonymous;
	};
};
int main()
{
	Tricky<int, 0> ok;
}

我们先来考虑前面一部分没有main()函数的例子。 标准C++编译器通常会编译这段模板定义, 来检查语法约束和一般的语义约束。 然而,在检查涉及到模板参数的约束时, 编译器会假设该参数“处于最理想的情况”(assume the best) 。 例如, 在模板Danger中, 用于成员Block的typedef(类型定义) 的参数N可能会是0或者负数(这就会是无效的) ; 但编译器会假设最理想的情况: 即参数N不会是0或者负数, 而是正整数。 类似地, 在成员no_body_here()声明中的缺省实参规范(=3) 也是可疑的, 因为不一定能够使用整数来对模板Safe进行初始化;但编译器会假定: 对于 Safe<T>的泛型定义, 并不会用到该缺省实参。类似地, 对于成员 error(), 如果没有注释掉, 那么在编译模板的时候,它将会引发一个错误, 因为使用Danger<0>会被要求给出类Danger<0>的完整定义, 而产生这个类的定义会试图typedef一个元素个数为0的数组(即Block[0]) 。 因此, 即使成员error()没有被使用, 并因此而不会被实例化, 但是仍然会引发一个错误。 这个错误是在泛型模板的处理过程中引发的。 然而, 与error()相反的是成员unsafe(T (*p)[N])的声明, 在N还没有被模板参数替换之前, 该声明是不会产生错误的。

现在让我们来分析添加 main()函数后会出现什么样的结果。 它会使编译器替换模板Tricky的参数: 用int替换T, 用0替换N。 实际上, 这里并不需要Tricky中所有成员的定义, 但缺省构造函数(在这个例子该函数是隐式声明的) 和析构函数是肯定会被调用的, 因此它们的定义必须存在。 实际上, 还需要提供虚拟成员(如虚函数) 的定义, 否则的话可能就会引发一个链接期错误。 譬如, 如果我们既没有注释掉虚函数成员suspect()的声明, 也没有提供它的定义的话, 链接器就会给出这类错误。 相反, 对于成员inclass()和结构(struct) Nested的定义, 它们会要求一个完整的 Danger<0>类型(而我们从前面讨论已经知道, 该完整类型会包含一个无效的 typedef) ; 但因为程序中并不会用到这两个成员的定义, 因此不会产生它们的定义, 从而也就不会引发错误。 另一方面, 所有的成员声明都是会被生成的, 而且作为我们(用实参) 替换后的结果, 这些声明将可能会包含无效类型, 而这是不允许的。 譬如, 如果没有注释掉unsafe(T (*p)[N])声明, 我们将会再次创建一个元素个数为0的数组类型, 同样会引发一个错误。 类似地, 在匿名union中, 如果我们用Danger<N>替换(源代码中的) Safe<T>, 也会引发一个错误, 因为类型Danger<0>并不是完整的, 也是无效的。

 

10.3 C++的实例化模型

模板实例化是这样的一个过程: 根据相应的模板实体, 适当地替换模板参数, 从而获得一个普通类或者函数。 这个定义听起来很简单明了, 但在实际应用中我们需要遵循许多细节。

对于该章节,第一次看的时候还是不怎么懂的,不过下面的文章写得也是比较好的,可以结合一起看:http://www.cnblogs.com/claruarius/p/4067868.html

 

10.3.1 两阶段查找

从第9章中我们知道: 当对模板进行解析的时候, 编译器并不能解析依赖型名称于是, 编译器会在POI(point of instantiation, 实例化点) 再次查找这些依赖型名称。 另一方面, 非依赖型名称是在首次看到模板的时候就进行查找, 因此在第1次查找时就可以诊断错误信息。 于是, 就有了两阶段查找(two-phase lookup) 这个概念: 第1阶段发生在模板的解析阶段, 第2阶段发生在模板的实例化阶段

在第1阶段, 当使用普通查找规则(在适当的情况也会使用ADL)对模板进行解析时, 就会查找非依赖型名称。 另外, 非受限的依赖型名称(诸如函数调用中的函数名称, 之所以说它是依赖型的, 是因为该名称具有一个依赖型实参) 也会在这个阶段进行查找, 但它的查找结果是不完整的(就是说查找还没结束) , 在实例化模板的时候, 还会再次进行查找。

第2阶段发生在模板被实例化的时候, 我们也称此时发生的地点(或者源代码的某个位置) 为一个实例化点 POI。 依赖型受限名称就是在此阶段进行查找的(查找的目标是: 运用模板实参代替模板参数之后所获得的特定实例化体) ; 另外, 非受限的依赖型名称在此阶段也会再次执行ADL查找

 

10.3.2 POI

从上面我们知道, C++编译器会在模板客户端代码中的某些位置访问模板实体的声明或者定义。 于是, 当某些代码构造引用了模板特化,而且为了生成这个完整的特化, 需要实例化相应模板的定义时, 就会在源代码中产生一个实例化点(POI) 。 我们应该清楚, POI是位于源代码中的一个点, 在该点会插入替换后的模板实例

对于非类型(函数)的特化,例如:

class MyInt {
public:
	MyInt(int i);
};

MyInt operator- (MyInt const&);
bool operator> (MyInt const&, MyInt const&);
typedef MyInt Int;

template <typename T>
void f(T i)
{
	if (i > 0) {
		g(-i);
	}
} 
//(1)

void g(Int)
{
	//(2)
	f<Int>(42); //调用点
	//(3)
} //(4)

当C++编译器看到调用f<Int>(42)时, 它知道需要用MyInt替换T来实例化模板f: 即生成一个POI。 (2) 处和(3) 处是临近调用点的两个地方, 但它们不能作为POI, 因为C++并不允许我们把::f<Int>(Int)的定义在这里插入。 另外, (1) 处和(4) 处的本质区别在于: 在(4) 处,函数 g(Int)是可见的, 而(1) 处则不是; 因此在(4) 处函数 g(-i)可以被解析。 然而, 如果我们假定(1) 处作为POI, 那么调用g(-i)将不能被解析, 因为g(Int)在(1) 处是不可见的。 幸运的是, 对于指向非类型(也就是函数)特化的引用, C++把它的POI定义在“包含这个引用的定义或声明之后的最近名字空间域”中。 在我们的例子中, 这个位置是(4) 。

你可能会疑惑我们为什么在例子中使用类型MyInt, 而不直接使用简单的int类型。 这主要是因为: 在POI执行的第2次查找(指g(-i)) 只是使用了ADL。 而基本类型int并没有关联名字空间, 因此, 如果使用int类型, 就不会发生ADL查找, 也就不能找到函数g(原因见10.3.5小节)。 所以, 如果你用下面的typedef代替原来的typedef:

typedef int Int;

那么前面的例子将不能通过编译。

对于类特化, POI的位置是不一样的。 可以通过下面代码来说明:

template<typename T>
class S {
public:
	T m;
};
//(5)
unsigned long h()
{
	//(6)
	return (unsigned long) (sizeof(S<int>));
	//(7)
} //(8)

如前所述, 我们知道位置(6) 和(7) 不能作为POI, 因为名字空间域类S<int>的定义不能出现在这两个位置(模板是不能出现在函数作用域内部的) 。 如果我们采用前面非类型实例的规则, 那么 POI 应该在(8) 处, 但这样的话, 表达式 sizeof(S<int>)会是无效的, 因为要等到在编译到(8) 之后, 我们才能确定S<int>的大小, 而代码sizeof(S<int>)位于(8) 之前。 因此, 对于指向产生自模板的类实例的引用, 它的POI只能定义在“包含这个实例引用的定义或声明之前的最近名字空间域。在我们这个例子中, 是指位置(5) 。

在实例化模板的时候, 可能还需要进行某些附带的实例化。 考虑下面的简短例子:

template<typename T>
class S {
public:
	typedef int I;
};
//(1)
template<typename T>
void f()
{
	S<char>::I var1 = 41;
	typename S<T>::I var2 = 42;
}
int main()
{
	f<double>();
} //(2) ,此处分为: (2a), (2b)

根据前面的讨论, 我们知道f<double>的POI会在(2)处。 但在这个例子中, 函数模板f()引用了一个类特化S<char>; 从前面的讨论我们知道, 该类特化的POI应该在(1)处。 另外, 函数模板f()还引用了S<T>; 因为S<T>是依赖型的, 所以我们不能像S<char>那样来确定它的POI。 然而, 如果在(2)处实例化了f<double>, 我们知道同时需要实例化S<double>的定义。 对于类型实体和非类型实体, 这种二次(或者传递) POI(指S<double>的POI) 的定义位置稍微有些区别。 对于非类型实体, 这种二次POI的位置和主POI(指f<double>)的位置相同对于类型实体, 二次POI的位置位于主POI(指f<double>)位置的紧前处(最近的名字空间域内)。在我们的例子中,利用前面的规则,f<double>的POI位于(2b)处, 而在它的紧前处(即(2a)处),就是二次POI(即S<double>的POI) 的位置。 现在我们就知道S<double>和S<char>的POI是不同的。

一个翻译单元通常会包含同个实例的多个 POI。 对于类模板实例而言, 在每个翻译单元中, 只有首个POI会被保留, 而其他的POI则被忽略(其实它们并不会被认为是POI) 。 对于非类型实例而言, 所有的POI都会被保留

 

10.3.5 例子

下面的一些例子很好地说明了我们前面所描述的一些概念。

第1个是关于包含模型的简单例子:

emplate <typename T>
void f1(T x)
{
    g1(x); //(1)
}
void g1(int)
{ }

int main()
{
    f1(7); //错误, 找不到g1!
} //(2):f<int>(int)的POI

调用f1(7)将会产生f1<int>(int)的一个POI, 它紧跟main()函数的后面(即(2) 处) 。 在这个实例中, 关键的问题是函数g1的查找。 当第一次看到模板f1的定义时, 编译器注意到非受限名称g1是一个依赖型名称, 因为它的参数名称依赖于外部函数f的模板参数(即实参x的类型依赖于模板参数T) 。 因此, 编译器会在(1)处使用普通查找规则来查找g1, 然而在(1)处并不能看到g1, 从而第1阶段找不到g1。 在(2) 处, 即f1的POI, 会在关联名字空间和关联类中再次查找g1, 但由于g1的唯一实参类型是int, 而int并没有关联名字空间和关联类, 从而第2阶段也找不到g1。 因此, 尽管在f1的POI处(即(2) 处) 可以使用普通查找规则找到g1(这只是一个假象而已) , 但是根据我们前面的分析, 该例子实际上并不能找到g1。

 

10.5 显式实例化

为模板特化显式地生成POI是可行的, 我们把获得这种特化的构造称为显式实例化指示符(explicit instantiation directive) 。 从语法上讲,它由关键字template和后面的特化声明组成, 所声明的特化就是即将由实例化获得的特化。 例如:

template<typename T>
void f(T) throw(T)
{ } //
4个有效的显式实例化体:
template void f<int>(int) throw(int);
template void f<>(float) throw(float);
template void f(long) throw(long);
template void f(char);

上面每个实例化指示符都是有效的。 模板实参可以通过演绎获得, 异常规范也可以省略, 如果没有省略的话, 异常规范就必须匹配相应的模板。

类模板的成员也可以使用这种方式来进行显式实例化:

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

template void S<int>::f();
template class S<void>;

另外, 通过显式实例化类模板特化本身, 同时就显式实例化了类模板特化的所有成员

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值