C++模板编程(20)---C++实例化模型C++ Instantiation Model

所谓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). 两次都是合理的,因此调用具有二义性,不合法

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值