站在对象模型的尖端

一、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的“实例化”行为

template<class Type>  
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<Type> *freeList;  
    static int chunkSize;  
    Type _x,_y,_z;  
};  

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

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

Point<float>::Status s;//正确  
Point::Status s;//错误  

//同样freeLsit和chunkSize对程序而言也不可用
Point::freeList;//错误  
Point::chunkSize;//错误  
Point<float>::freeList;//正确  
Point<float>::chunkSize;//正确  

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

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

  都会导致template class 的实例化。

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

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

  1. 空间和时间效率的考虑。因为一个类可能会有很多member functions,但一个class实例并不一定会用到所有的member function。
  2. 尚未实现的机能。例如:origin的定义需要调用Point的默认构造函数和析构函数,因此只有这两个函数需要被实例化。

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

Template的错误报告

  在template class中,所有与类型有关的检验,如果牵扯到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 type>  
class ScopeRules  
{  
public:  
    void invariant()  
     {  
            _member = foo(_val);  
     }  
     type type_dependent()  
     {  
            return foo(_member);//书中此处我觉得有问题,因为type的具体值是什么还不知道  
              //那么如果类型不匹配就无法成功调用foo()函数。还请网友指点一下  
     }  
private:  
     int _val;  
     type _member;  
}; 

//scope of template instantiation  
extern int foo( int );  
ScopeRules<int> 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,其实也就是模板一般化和模板特化,一个用以一般的模板类,另一个用以专注于特定的实例。

二、异常处理

  当一个异常发生时,编译系统必须完成以下事情:

  1. 检验发生throw操作的函数
  2. 觉得throw操作是否发生在try区段中。
  3. 若是,编译系统必须把异常类型拿来和每一个catch子句进行比较。
  4. 如果比较吻合,流程控制应该交到catch子句手中。
  5. 如果throw的发生并不在try区段中,或没有一个catch子句吻合,那么系统必须(a)摧毁所有已构造的局部对象,(b)从堆栈中将木目前的函数“unwind”(解除)掉。(c)进行到程序堆栈的下一个函数中去,然后重复上述步骤2~5。

  当一个异常被抛出时,异常对象会被产生出来并通常放置在相同形式的异常数据堆栈中。从throw端传给catch子句的,是异常对象的地址、类型描述器(或是一个函数指针,该函数会传回与该异常类型有关的类型描述器对象)以及可能会有的异常对象描述器。

  c++的实现中,正常执行时,在每一个函数被推离堆栈之前,函数的 local class objects 的 destructor会被调用。

  如果exception抛出时,exception之前的local object 会析构,后面的代码不执行,所以才需要那些什么auto_ptr什么的包装一层,也就是在前面声明一些auto_ptr,即使遇到exception,auto_ptr 的析构函数也会被调用。

三、执行期类型识别

(Type-safe Downcast)保证安全的向下转换操作

  一个保证安全的向下转换操作必须在执行期对指针有所查询,看看它是否指向它所展现的对象的真正类型。因此,要想支持type-safe downcast,在对象空间和执行时间上都需要有一些额外负担:

  1. 需要额外的空间以存储类型信息,通常是一个指针,指向某个类型信息节点。
  2. 需要额外的时间以决定执行期的类型(run type),因为,这需要在执行期才能决定。

  C++的RTTI(运行时类型识别)机制提供了一个安全的downcast设备,但只对那些展现多态(也就是使用继承和动态绑定)的类型有效。

  通过声明一个或多个虚拟函数来区别class声明来分辨出这些类型。

  在C++中,一个具备多态性质的class,正是内含继承而来(或直接声明)的虚拟函数。

  c++ 的 RTTI(Run time type identification)执行期类型识别,有的编译器是在virtual table里的一个指针指向具体的 type_info结构来实现的。

  假如产生一个exVetex异常(exVetex继承自exPoint),下面两种写法有些不同:

//1.
catch(exPoint p){
    throw; //继续抛出的是exVetex,p是一个临时对象
}

//2.
catch(exPoint &p){
    throw;//继续抛出的是裁剪后的exPoint
}

  只有在一个catch子句评估完毕并且知道它不会再抛出exception之后,真正的exception object才会被销毁。

引用不是指针

  程序执行中对一个class指针类型施以dynamic_cast运算符,会获得true或false:

  1. 如果传回真正的地址,则表示这一对象的动态类型被确认了,一些与类型有关的操作现在可以施行于其上。
  2. 如果传回0,则表示没有指向任何对象,意味着应该以另一种逻辑施行于这个动态类型未确定的对象身上。

  dynamic_cast运算符也适用于引用上,然而对于一个non-type-safe cast,其结果不会与施行于指针的情况  相同。因为引用不可以像指针那样“把自己设为0来表示”no object“”。

  Waring:若将一个引用设为0,会引起一个临时对象被产生出来,且该对象的初值为0。

  因此,当dynamic_cast施行于一个引用时,会发生下列事情:

  1. 如果引用真正参考到适当的继承类,downcast会被执行而程序可以继续进行。
  2. 如果引用并不真正是某一种继承类时,那么,由于不能传回0,因此抛出一个bad_cast 异常。

typeid运算符

  typeid运算符,主要用于判断两个对象是否是同一个类。返回值为type_info。

  ypeid 返回的是一个class的typeInfo的结构。

posted on 2020-01-10 19:43  tianzeng  阅读(215)  评论(0编辑  收藏  举报

导航