所谓template实例化,是适当地替换template parameters,以便从template获得常规class或常规function的过程。听起来平淡无奇,但实际上这个过程涉及极多细节。
1. 两段式查询two-phase lookup
当编译器对template进行parsing词法分析时,无法解析受控名称dependent names。这些受控名称在对具现点被再次查询lookup。然而非受控名称non-dependent names会较早被查询,如此一来当template首次被编译器看到时,就可以较多地诊断处错误。这就是两段式查询概念:第一阶段发生在template parsing词法解析时刻,第二阶段发生在实例化时刻。
在第一阶段编译器会查询非受控名称non-dependent names,这个过程会用到常规查询规则ordinary lookup规则:如果情况适用也会动用ADL(argument-dependent lookup)规则。非受饰的受控名称(unqualified dependent names;由于它们在一个带有dependent arguments的函数调用中看起来像个函数名称,所以是受控的,dependent)也会以此方式被查询,然而其结果并不完备,因此会在template实例化时再次被查询。
在第二阶段,也就是一个所谓具现点point of instantiation, POI处,编译器会查询受控受饰名称(dependent qualified names;将特定具现体的template parameter代换为template arguments),也会对非受饰受控名称(unqualified dependent names)额外加以ADL查询。
2. 具现点Point of Instantiation,POI
我们已经说过,template程序代码中有这样一些位置点:C++编译器在这些点上必须能够取得某个template物体的声明或定义。一旦程序代码需要用到template的特化体,而编译器需要见到该template的定义以创造出该特化体时,就会在这个具现点POI上进行具现过程。具现点POI就是替换之后之模板template可插入的源码位置点。例如:
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)
当编译器看到调用动作f<Int>(42),它必须将T替换为MyInt来实例化template f。这于是就产生了一个POI,具现点。(2)与(3)距离很近,但它们不能作为POI,因为C++语法不允许在这两处插入::f<Int><Int>的定义。(1)和(4)的本质差异在于函数g(Int)在4处可见,于是template dependent call g(-i) 可被解析出来。如果把(1)当作POI,g(-1)就无法成功解析出来。幸运的是,针对“a reference to a nonclass specialization”, C++设计的POI是在最内层之namespace 作用域的声明或定义式的紧临后方。本例中这个POI就是(4)。
大家可能奇怪为什么本例使用MyInt而不是使用int。答案是在POI处进行的第二阶段查询只动用ADL。由于int并无相应的namespace,不会发生POI查询,从而编译器无法找到函数g()。如果你把Int的 typedef换成:
typedef int Int;
上述例子就无法通过编译。
template <typename T>
class S
{
public:
T m;
};
//(5)
unsigned long h()
{
//(6)
return (unsigned long) sizeof (S <int>);
//(7)
}
//(8)
函数作用域内的(6)和(7)不能作为POI,因为class S<int> (它构成一个namespace作用域)的定义式不能出现在这两个地方(原因是template不能在函数作用域内出现)。如果按照nonclass的方式思考,POI将是(8),但这样就造成算式S<int>不合法,因为在(8)之前S<int>的大小无从得知。因此对于“a reference to a template-generated class instance”,它们的POI位置被定义为:“最内层之namespace作用域(内含该class instance)“的声明语句或定义式的紧邻前方。本例中这个点是(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)。function template f()也用到了s<char> 这个class特化体,其POI在(1),还用到了S<T>,但在(1)处S<T>仍然受控(dependent),因此在(1)还不能进行实例化。然而,如果我们在(2)处实例化f<double> 我们必须同时实例化S<double>。这种级次POI(second POI, 或称transitive POI)的定义稍有不同。针对nonclass对象,次级POI和主POI完全相同,但针对class对象,次级POI紧邻其主POI之前。
上个例子中f<double>的POI在2(b),其前先的2(a)是S<double>的二级POI。
在一个编译单元中,同一个具现往往有多个POI。针对class template实体,编译器只保留头一个POI。忽略所有后续POI(编译器并不真正认为它们是POI)。针对nonclass实体。所有POI都被保留。无论哪种情况,ODR(单一定义原则)都要求:被编译器保留的所有POI彼此都必须等价,但编译器不需要检验是否有违例情况。编译器可以有一个nonclass POI进行真正的实例化动作,而不需要担心其他POI可能导致其他具现体。
事实上,大多数编译器都会把nonline function templates的真正实例化过程推迟到编译单元尾端。这也就是把相应的template特化体的POI移到了编译单元尾端。C++编译器实现者认为,这是一个合法的实现技术,但C++ 标准对此并无明确态度。
3. 置入式inclusion和分离式Seperation模型
无论何时遇到一个POI,编译器必须能够取得与之对应的template的定义。对于class 特化体而言,这意味着当前编译单元中,class template的定义式必须出现在POI之前。对nonclass POI的态度虽然也如此,但典型情况是nonclass template的定义式被放在表头文档中。由当前编译单元以#include将它包含进来。这种template定义式的源码组织方式被称为置入式模型inclusion model,也是大家比较喜欢采用的模型。
针对非类POI,还存在另一种方式: nonclass template 可被声明为export,并定义于另一个编译单元。这种方式称为分离式模型(separation model)。下面例子展示这种情况,仍然用的是max():
//Unit 1
#include <iostream>
export template <typename T>
T const & max(T const&, T const&);
int main()
{
std::cout << max(7, 42) << std::endl; //(1)
}
// unit2
export template <typename T>
T const& max(T const& a, T const& b)
{
return a < b ? b:a;
}
当第一个文件被编译时,编译器认为(1)是POI,此时T被替换为int。编译器必须确保编译单元2中的max()定义式被实例化,从而满足POI的需求。
4. 跨越编译单元寻找POI
假设上述的编译单元1被重写为:
//unit 1
#include <iostream>
export template <typename T>
T const & max(T const&, T const &);
namespace N
{
class I
{
public:
I(int i): v(i) {}
int v;
};
bool operator < (I const & a, I const & b) {
return a.v < b.v;
}
}
int main()
{
std::cout << max(N::I(7), N::I(42)).v << std::endl; //(3)
}
POI 诞生于(3)处,而且编译其需要编译单元2中的max()定义。然而max()使用重载的operator< ,而后者却是在编译单元1中被声明,不可见于编译单元2.为了正确处理这种情况,编译器必须在实例化过程中参考两个不同的声明上下文declaration context:一是template定义于何处;二是类型I声明于何处。为涉入这个两个上下文,编译器使用两段式查询来对付template中的名称。
第一阶段发生在template被parsing词法分析时候,换句话说在C++编译器首次见到template定义式时候,在此阶段,编译器会使用ordinary lookup规则和ADL规则来查询非受控名称non-dependent names。另外,编译器还使用ordinary lookup规则来查询受控函数dependent function,因为其函数自变量受控)的非受饰名称,但它会记住查询结果,不企图进行重载解析。重载解析是在第二阶段后进行的。
第二阶段发生在POI。在POI处,编译器会使用ordinary lookup 规则和ADL规则来查询受控受饰名称dependent qualified names。至于受控非受饰名称dependent unqualified names 已经在第一阶段以ordinary lookup 查询过,编译器只运用ADL规则去查询,再把查询结果结合第一阶段的结果。这两个结果构成的集合将被用来完成重载函数的解析过程overload function resolution。
5.举例
第一例子是置入式模型的inclusion model的简单情况:
template <typename T>
void f1(T x)\
{
q1(x); //(1)
}
void g1(int)
{
}
int main()
{
f1(7); //Error, 找不到g1()
//(2) f1<int>(int) 的POI
}
调用f1(7)会制造出一个POI,位于main()的紧邻外侧(2)处。这个实例化过程的关键在于函数g1()的查询。
当 template f1的定义式首次出现,编译器会注意到非受饰名称g1是受控的dependent,因为它是带有受控自变量(dependent argument):自变量的x类型取决于template parameter T的函数调用实参的函数名称。因此在(1)处使用ordinary lookup查询规则找g1,此刻不可见。在(2)处,也就是POI,编译器再次于相应的namespace和classes中查询g1,但因g1()唯一的自变量是int类型,而int类型没有相应的namespace和classes,所以编译器最终没有找到g1的完整类型。
第二个例子:
//common.hpp
export template <typename T>
void f(T);
class A {
};
class B{
};
class X{
public:
operator A() {return A();}
operator B() {return B();}
};
//a.cpp
#include "common.hpp"
void g(A)
{
}
int main()
{
f<X>(X());
}
//b.cpp
#include "common.hpp"
void g(B)
{
}
export template <typename T>
void f(T x)
{
g(x);
}
在文件 a.cpp中,main() 调用 f<X>(X()).f是个exported template,定义于文件b.cpp,
其内的调用动作g(x)被自变量类型x实例化。g()被查询两次:第一次使用ordinary lookup在文件b.cpp中查询(模板解析时候),第二次使用ADL规则在文件a.cpp中查询(模版实例化动作所在)
第一次查询找到g(B);第二次查询找到g(A). 两次都是合理的,因此调用具有二义性,不合法