C++ Template实例化(13)---《C++ Templates》

来源:互联网 发布:淘宝发错货赔偿规则 编辑:程序博客网 时间:2024/06/06 02:06

上篇博客我们讲了C++ Templats中class template和function template的特化以及function template的重载,作为特化的好兄弟,我们今天讲一下C++ Template中的实例化。
实例化包括隐示实例化以及显示实例化,其中隐示实例化写法简单,但背后是C++编程的强大的后台支持代码;显示实例化程序员调用起来可能复杂一些,但是简单易懂,各有各的特点,所以现在我们分别来讲一下这些区别:

隐示实例化

当C++编译器看到我们调用
隐示实例化意味着当程序用到template时候,编译器需要得知该template和某些成员的完整定义(而不仅仅是template的声明)。参看如下代码:

template <typename T>class C;//(1)只有声明C<int>* p=0;//(2)不需C<int>的定义template <typename T>class C{public:    void f();//(3)成员声明};  //(4)class template定义完整void g(C<int>& c){//(5)只用到class template        c.f();//用到了class template的定义:需要C::f()的完整定义}

在(1)处,编译器只能看到template的声明,见不到其定义(这种声明又称为前置声明)。和常规的classes的规则一样不需要class template定义就可以声明该类型的pointers或者引用。例如函数g()的参数类型是C<int>&并不需要template C的完整定义。然而一旦编译器需要知道某个template物体的大小,或者程序代码中去用了该特化体的某个成员,编译器就必须见到template的定义。这说明为什么在(6)出,class template的定义必须可见;如果见不到template的定义,编译器就无法判断这个成员是否存在,或者程序代码是否有权使用它。
这而有另外一个表达式,需要class template具现体,因为此处编译器必须知道C<void>的大小:

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

这里必须完成实例化,这样编译器才可以知道C<void>的大小。然而这而tamplate C的参数T用什么类型替换,template具现体的大小都不会收到影响:任何情况下C<X>都是个empty class。但你不能要求编译器知道这一点。不仅如此,在本例中,编译器也需要以实例化过程得知C<void>是否有一个可被取用的default构造函数,并确保C<void>没有定义private operator new和private operator delete。

template <typename T>class C{public:    C(int);};void candidate(C<double> const&);void candidate(int){}int main(){    candidate(42);    return 0;}

根据偏序规则,C++编译器将其解析为第二个,因为function template和non-template function进行重载时候,我们首先考虑非模板函数,因此,我们选择non-template function版本。

缓式实例化(lazy Instantiation)

很多情况下,编译器需要一个完整的class类型,运用template时,编译器会从class template中产生出一个完整的定义。那我们就需要思考一个问题?template中到底需要多少内容需要被实例化?答案是:只有真正被用到的那些才能被实例化,即编译器需要尽量延迟template实例化的执行,lazy Instantiation由此得名。

当class template被隐式实例化,其中每个成员声明都被同时实例化,那么定义呢?答案是并非所有对应的定义都被实例化,其中有些例外:1)如果class template中具有一个不具名的union,该union成员都会被实例化;2)virtual成员函数,当class template需要被实例化时,virtual成员函数可能也可以不被实例化。
当template被实例化时default call arguments通常被个别考虑除非有些调用真正用上了default arguments,否则这些default arguments不会被实例化。如果调用函数时明确制定了arguments,则default arguements也不会别实例化:

template <typename T>class Safe{};template <int N>class Danger{public:    typedef char Block[N];};template <typename T,int N>class Tricky{public:    virtual ~Tricky(){    }    void no_body_here(Safe<T>=3);    void inclass(){        Danger<N> no_boom_yet;    }    T operator->();    struct Nested{        Danger<N> pfew;    };    union{        int align;        Safe<T> anonymous;    };};int main(){    Tricky<int,0> ok;    return 0;}

我们可以发现预设自变量Safe<T>=3不可以,因为template Safe无法以一个整数值初始化,但编译器会假定Safe<T>的泛化定义并不需要这个预设自变量。

C++实例化模型

  • 两段式查询

当编译器对template进行parsing时,它无法解析受控名称。这些受控名称将在具现点被再次查询。然而非受控名称会被焦躁查询,这样一来当template首次被编译器看到时,就可以较多地诊断出错误,这就是两段式查询的定义:第一段发生在template parsing(词法解析)时刻,第二段发生在实例化时刻。

第一段即template parsing阶段,这个过程会用到常规查询规则:ADL规则;
第二阶段,也就是在所谓的具现点(POI,point of instantiation)处,编译器会查询受控名称,将特定具现体的template parameters替换为template arguments,也会对非受控受控名称施加ADL查询。

  • 具现点(POI)—实例化时刻

template程序代码中有这样的一些位置点:C++编译器在这些点上必须能够取得某个template物体的声明或者定义,一旦程序代码需要用到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 g(-i)就可以被解析出来。如果将(1)当做POI,就无法解析g(-i),因为编译器无法看到g(Int),所以我们此处的POI就是(4)。
注意:针对“a reference to a nonclass specialization”C++设计的POI是最内层的namespace 作用域(惟需内含该reference)的声明项或者定义式的紧邻后方。

当template被实例化时候,有可能引发其他实例化动作:

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>();    return 0;}//(2)

根据先前讨论,f<double>的POI是(2),function template f()也用到了S<char>这个class的特化体,其POI在(1)。还用到了S<T>,但在移除任然受控,因此(1)出无法进行实例化,然而如果我们在(2)出实例化f<double>,我们同时需要实例化S<double>。这种次级POI的定义稍有不同。针对nonclass物体,次级POI和主要POI完全相同,但针对class物体,次级POI近邻与其基本POI之前。

在同一个编译单元中,同一个具现体往往有多个POI。针对class template实体,编译器只保留头一个POI。

实际上,大多数编译器都会把对noinline function template的真正实例化过程推迟至编译器尾端。这也就是把相应的template特化体的POI已到了编译单元尾端。

  • 置入式和分离式模型
    无论何时遇到一个POI,编译器必须能够取得对应的template定义。对于class特化体而言,这意味着当前编译单元中,class template的定义式必须出现在POI之前。对nonclass POI的态度虽然也是如此,但是典型的情况是nonclass template的定义式被放在头文档中,由当前编译单元以#include 将其包含进来,这种template定义式的源码组织方式称为置入式模型。

针对nonclass POI也还存在了一种方式:nonclass template可被声明为export,并且定义为另一个编译单元。这种方式成为分离式模型,参考如下代码理解:

#include <iostream>export template<typename T>T const& max(T const&,T const&);int main(){    std::cout<<max(7,42)<<std::endl;    return 0;}
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的需求。

  • 跨越编译单元寻找POI
#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)    return 0;}

第一阶段发生于template被parsing的时候,换句话说,C++编译器在首次见到template定义式时,在此阶段,编译器会使用ordinary lookup规则和ADL规则来查询非受控名称。另外还会查询非受控函数的非受控名称,记住查询结果但不进行重载解析;
第二阶段发生在POI处,在POI处,编译器会查询受控名称。

明确实例化

明确指定某template特化体的POI也是可能的,称为明确实例化的指令。语法上规定,明确实例化指令有、由关键词template以及待实例化之特化体的声明构成:

template <typename T>void f<T> throw(T){}//四个合法的明确实例化指令template void f<int>(int) throw(int);template void f<>(float) throw(float);template void f(long) throw(long);template f(char);

注意上述所有实例化指令都合法,template arguments可被编译器推导而出,异常规格可以省略,如果没有省略,就必须和temlate的的哪一个匹配。
class template的成员也可以这样明确实例化:

template <typename T>class S{public:    void f(){    }};template void S<int>::f();template class S<void>;

不仅如此,明确实例化一个class template特化体,会使所有成员都被明确实例化。
C++ Standard规定,在一个程序中,每个template特化体最多只能有一次明确实例化。如果一个template特化体被明确实例化,就不能被明确特化;反之亦然。

template <typename T>void toast(T const& x){    ...}
#include "toast.hpp"template void toast(float);

这是我们关于实例化的解析,如果库作者也将toast<float>明确实例化了,这样会使得客户端程序代码非法。为了避免这种情况,C++规定如果一个明确实例化指定接在一个针对相同物体的明确特化之后,那么该指令不具任何效果。

Templates明确实例化的当前限制的第二个限制是:如何提高编译速度。惟一一个具有可移植性并且能阻止在其他单元发生实例化的方法是:不要提供template定义式,除非是在它被明确实例化的编译单元内。

template <typename T>void f();void g(){    f<int>();}
template <typename T>void f(){}template void f<int>();//手动实例化void g();int main(){    g();    return 0;}

这种解法运作良好,但程序员必须拥有提供template接口之源码文件的控制权。通常程序员无法拥有这样的控制权,提供template接口的源码文件无法被修改,而且它们通常会提供template定义式。

一个比较有用的技巧是,在所有编译单元中声明某个template特化体,这样可以禁止该特化体被自动实例化,唯有在特化被明确实例化的编译单元中例外,代码如下

template <typename T>void f(){}template <>void f<int>();void g(){    f<int>();}
template <typename T.void f(){}template void f<int>();//手动实例化void g();int main(){    g();    return 0;}

这种代码基于如下假设:对一个明确特化之特化体的调用等价于对一个相匹配的泛型特化体的调用。这个假设并不正确,很多C++编译器为这连个物体生成了不同的重整名称。如果使用这种编译器,obj码就无法被链接成一个完整的exe文件。
有的编译器支持以下的扩展声明

template <typename T>void f(){}extern template void f<int>();//声明但是未曾定义void g(){    f<int>();}

PS:
如果大家对特化和实例化还存在其他疑问的话,推荐大家可以仔细阅读这篇博客:http://blog.csdn.net/imxiangzi/article/details/50263539,一定会有颇多感触的。