第7章 站在对象模型的尖端1: Template

一、template的三个主要的讨论方向:

  1. template的声明,也就是说当你声明一个template class、template class member function等时,会发生什么事情。

  2. 如何”实例化“class object、inline nonmember以及member template functions。这些是”每一个编译单位都会拥有一份实例“的东西。

  3. 如何”实例化“nonmember、member template functions以及static template class members。这些都是每一个可执行文件中只需要一份实例的东西。

二、Template的“实例化”行为

template class实例化

template  
class Point  
{  
public:  
    enum Status { unallocated, normalized };  
    Point( Type x = 0.0, Type y = 0.0, Type z = 0.0 );  
    ~Point();  
     
    void* operator new(size_t);  
    void operator delete(void*,size_t);  
private:  
    static Point *freeList;  
    static int chunkSize;  
    Type _x,_y,_z;  
};

当编译器看到template class的时候不会有任何反应,也就是说static data member并不可用,嵌套enum也一样。

其中,enum Status的真正类型在所有的Point 实例中都一样。但我们依旧只能通过template Point class的某个实例来存取和操作。即我们可以这样写:

Point::Status s;//正确  
Point::Status s;//错误  

//同样freeLsit和chunkSize对程序而言也不可用
Point::freeList;//错误  
Point::chunkSize;//错误  
Point::freeList;//正确  
Point::chunkSize;//正确  
// case1:
Point< float> *ptr=0;
// 程序什么也不会发生,因为指向的是 class object的指针
// 本身并不是一个class object, 编译器不需要知道与该class
// 任何有关的任何member数据或者object数据布局

// case2: 若是引用
const Point &ref = 0;
//此时,编译器会实例化一个Point的float实例,它会被扩展为一下形式:
//内部扩展  
Point temp(float(0));  
//因为引用并不是无物的代名词,0被视为整数,必须被转化为一下类型的一个对象
const Point &ref = temp;  

// case3
Point 
//一个class object的定义,不论是由编译器暗中的做(像temp那样),或是由程序员显式的做
const Point origin;

case2 和 case3 都会导致template class 的实例化。

template member实例化

然而,member function(成员函数)(至少对那些未被使用过的)不应该被实例化,只有在被使用的时候,C++标准才要求它们被实例化。

由使用者来主导”实例化“规则主要有两个原因:

  1. 空间和时间效率的考虑。因为一个类可能会有很多member functions,但一个class实例并不一定会用到所有的member function。

  2. 尚未实现的机能。例如:origin的定义需要调用Point的默认构造函数和析构函数,因此只有这两个函数需要被实例化。

实例化这些函数:目前有两种策略:(1)在编译的时候;(2)在链接的时候。

Template的错误报告

template< class T >
class Mumble
{
public$: //第一处错误,非法标识符$
  Mumble( T t=1024 ) //第二处错误,t被初始化1024,但如果我们给mumble绑定的是char呢?
  :_t(t) //第三处错误,_t并不是哪一个mumble中的成员,tt才是。这种错误一般会在类型检查这个阶段查找出来。每一个名称必须绑定在一个定义身上,要不就会被判定错误!
  {
     if( tt != t ) //第四处错误, !=运算符有可能还没有定义好【视T的真正绑定类型来定】,和第二点一样,只有template的各个实体才能诊断出来。
       throw ex ex; //第五处错误,非法标识符ex ex;这种错误会在编译时起的解析阶段被发现,c++语言中一个合法的句子不允许一个标识符后面紧跟一个标识符。
    }
private:
  T tt;
};

 在一个non template class声明中,这五个错误会在编译时期就能查出来。但template class却不同,举个例子,所有与类型有关的检验,如果牵扯到template参数,都必须延迟到真正的具现【instantiation】操作时才发生,也就是说,第二处错误与第四处错误会在每个具现【instantiation】操作时才能被检查出来,其结果会因为不同的实际绑定类型而不同,比如:

Mumble m; //合法的

则第二处与第四处就不是错误,但如果是

Mumble pm; //非法的

 那么第四处依然正确,但第二处肯定是错误!因为在c++中,不允许将一个除了0之外的整数常量赋值给指针。
那么,什么样的错误会在编译器处理template声明时被找出来?这里有一部分和template的处理策略有关。cfront对template的处理是完全解析【parse】,但不做类型检验,只有在每一个具现【instantiation】操作时才做类型检验,所以在这种parse策略之下,所有的语汇错误解析错误都会在处理template声明时被找出来

语汇分析器【lexical analyzer】会在第一处错误那里捕捉到一个非法标识符,解析器【parser】会这样标识它:(  public$: // caught ),表示这是一个不合法的卷标【label】,但解析器不会把“对一个未命名的member成员做出参考操作”视为错误,所以第三处错误并不归解析器负责,但解析器仍然会抓住第五处错误。

在一个十分普遍的替代策略中,template声明被收集为一系列的“lexical tokens”,而parsing操作延迟到有真正具现【instantiation】操作时才开始:每当看到一个instantiation,相关的token就会被推往parser,然后调用类型检验查找有无错误等等;面对先前出现的那个template声明,“lexical tokenizing”会指出什么错误吗?事实上很少,只有第一处错误那里的非法标识符会被找出,其余的template声明都会被解析为合法的tokens并收集起来!

 Nonmember和member template function在具现【instantiation】发生之前也没有做到完全的类型检验,这导致某些十分离谱的错误竟然可以编译通过,例如下面这个:

template
class Foo
{
public:
  Foo();
  type val();
  void val(type v);
private:
  type _val;
}; //以上这些没啥问题
...
template
double Foo::hello(){ return this->world; } //错误在这里!看里面的hello和world,我们可有在Foo里面声明?

 不论是cfront还是Sun编译器亦或borland,都不会对上面的代码产生怨言!
再说一次,上面的策略,都是编译器设计者自己的决定,template facility并没有说不允许对template声明的类型部分有更严格的检查,当然,其实这样的错误可以在编译时起发现,只不过大家懒得这么做罢了。

Template中的名称决议法

1.scope of the template definition(定义出template的域)
2.scope of the template instantiation(实例化template的域)

//scope of the template definition   
extern double foo( double );  
  
template  
class ScopeRules  
{  
public:  
    void invariant()  
     {  
        _member = foo(_val);  
     }  
     type type_dependent()  
     {  
        return foo(_member);  
     }  
private:  
     int _val;  
     type _member;  
}; 
//scope of template instantiation  
extern int foo( int );  
ScopeRules sr0;

此时如果有以下调用:

sr0.invariant();  
//那么在invariant()中调用的究竟是哪个foo()函数实例?答案是
extern double foo( double );

因为,在 Template 中,对于一个nonmember name(非成员名称)的决议结果,是根据这个name的使用是否  与”用以实例化该template的参数类型“有关而决定的。

  1. 如果不相关,就使用scope of the template definition来决定name;

  2. 如果相关,就使用scope of template instantiation来决定name;

如果此时有如下调用:

sr0.type_dependent();

那么此时会调用scope of template instantiation中声明的foo()函数,而在该例子中,共有两个foo()函  数,且此例的_member类型为int,所以调用

extern int foo( int );  

//如果是
ScopeRules< double > sr0;  
//那么就会调用
extern double foo( double );

不管如何演变,都是由“scope of template instantiation”来决定。

总结如下

scope of template definition//用以专注于一般的template class  
scope of template instantiation//用以专注于特定的实例

三、Member function的实例化行为【部分总结】

template functions的实例化:

目前有两个策略,一个是编译时期策略,另一个是链接时期策略。

但这两个策略都有一个共同的缺点:当template实例被产生出来时,有时候会大量增加编译时间。

总结

因为模板类型不确定,所以对一个模板类型的变量赋初值可能会是错误的。因为模板类型不确定,所以并不是所有运算符都会支持。模板最后应该以分号结束。因为,在模板类中,所有关于类型的检查会延迟到实例化之后才会发生。

一个编译器要保持两个scope contexts,其实也就是模板一般化和模板特化,一个用以一般的模板类,另一个用以专注于特定的实例。

深度探索c++对象模型之template的错误报告_c++ 引入 template 错误-CSDN博客

你可能感兴趣的:(深入探索C++对象模型,前端)