zoukankan      html  css  js  c++  java
  • 深入探索C++对象模型(七)

    站在对象模型的尖端(On the Cusp of the Object Model)

    Template

    下面是有关template的三个主要讨论方向:

    1. template的声明,基本上来说就是当你声明一个template class、template class member function等等,会发生什么事情。
    2. 如何"具现(instantiates)"出class object以及inline nonmember,以及member template functions,这些是"每一个编译单元都会拥有的一份实体"的东西。
    3. 如何“具现”出nonmember以及member templates functions,以及static template class members,这些都是"每一个可执行文件中只需要一份实体"的东西,这也就是一般而言template所带来的问题。

    Template的"具现"行为(Template Instantiation)

    考虑下面的template Point class:

    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 members并不可用。nested enum或其enumerators也一样。

    虽然enum Status的真正类型在所有的Point instantiations中都一样,其enumerators也是,但它们每一个都只能通过template Point class的某个实体来存取或操作,因此我们可以这样写:

    Point<float>::Status s;
    

    但是不能这样写:

    //error
    Point::Status s;
    

    同样的道理,freeList和chunkSize对程序而言也还不可用,我们不能够写:

    //error
    Point::freeList;
    

    我们必须明确地指定类型,才能使用freeList:

    Point<float>::freeList;
    

    像上面这样使用static member,会使其一份实体与Point class的float instantiation在程序中产生关联,如果我们写:

    //ok, 另一个实体(instance)
    Point<double>::freeList;
    

    就会出现第二个freeList实体,与Point class的double instantiation产生关联

    一个class object的定义,不论是由编译器暗中地做,或是由程序员像下面这样明确地做:

    const Point<float> origin;
    

    都会导致template class的“具现”,也就是说,float instantiation的真正对象布局会被产生出来。

    member functions(至少对于那些未被使用过的)不应该被“实体”化,只有在member functions被使用的时候,C++ Standard才要求它们被“具现”出来。当前的编译器并不精确遵循这项要求,之所以由使用者来主导“具现”规则,有两个主要原因:

    1. 空间和时间效率的考虑。如果class中有100个member functions,但你的程序只针对某个类型使用其中两个,针对另一个类型使用其中5个,那么其他193个函数都“具现”将花费大量的时间和空间。
    2. 尚未实现的功能,并不是一个template具现出来的所有类型就一定能够支持一组member functions所需要的所有运算符。如果只“具现”那些真正用到的memeber functions,template就能够支持那些原本可能会造成编译时期错误的类型(types)。

    Template中的名称决议方式(Name Resolution within a Template)

    你必须能够区分以下两种意义。一种是C++ Standard所谓的"Scope of the template definition",也就是“定义出template”的程序。另一种是C++ Standard所谓的"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);
        }
        //...
    private:
        int _val;
        type _member;
    };
    

    第二种情况举例如下:

    //scope of the template instantiation
    extern int foo(int);
    //...
    ScopeRultes<int> sr0;
    

    在ScopeRules template中有两个foo()调用操作。在“scope of template definition”中,只有一个foo()函数声明位于scope之内。然而在“scope of template instantiation”中,两个foo()函数声明都位于scope之内。如果我们有一个函数调用操作:

    //scope of the template instantiation
    sr0.invariant();
    

    那么,在invariant()中调用的究竟是哪一个foo()函数实体呢?

    //调用的是哪一个foo()函数实体
    _member = foo(_val);
    

    在调用操作的那一点上,程序中的两个函数实体是:

    //scope of the template declaration
    extern double foo(double);
    
    //scope of the template instantiation
    extern int foo(int);
    

    而_val的类型是int,那么你认为选中的是哪一个呢?结果,被选中的是直觉以外的那一个:

    //scope of the template declaration
    extern double foo(double);
    

    Template之中,对于一个nonmember name的决议结果是根据这个name的使用是否与“用以具现出该template的参数类型”有关而设定的。如果其使用互不相关,那么就以“scope of the template declaration”来决定name。如果其使用互有关联,那么就以“scope of template instantiation”来决定name。在第一个例子中,foo()与用以具现ScopeRules的参数类型无关:

    //the resolution of foo() is not
    //dependent on the template argument
    _member = foo(val);
    

    这是因为_val的类型是int, _val是一个“类型不会变动”的template class member。也就是说,被用来具现出这个template的真正类型,对于 _val的类型并没有影响。此外,函数的决议结果只和函数的原型(signature)有关,和函数的返回值没有关联。因此, _
    member的类型并不会影响哪一个foo()实体被选中。foo()的调用与template参数毫无关联! 所以调用操作必须根据"scope of the template declaration"来决议。在此scope中,只有一个foo()候选者。

    让我们另外看看"与类型相关"(type-dependent)的用法:

    sr0.type_dependent();
    

    这个函数的内容如下:

    return foo(_member);
    

    它究竟会调用哪一个foo()呢?

    这个例子很清楚地与template参数有关,因为该参数将决定_member得真正类型。所以,这一次foo()必须在"scope of the template instantiation"中决议,本例中这个scope有两个foo()函数声明。由于 _member的类型在本例中为int,所以应该是int版的foo()出线。如果ScopeRules是以unsigned int或long类型具现出来,那么foo()调用操作就暧昧不明。最后,如果ScopeRules是以某一个class类型具现出来,而该class没有针对int或double实现出convertion运算符,那么foo()调用操作会被标识为错误。不管如何改变,都是由"scope of the template instantiation"来决定,而不是由"scope of the template declaration"决定。

    这意味着一个编译器必须保持两个scope contexts:

    1. “Scope of the template declaration”,用以专注于一般的template class
    2. "Scope of the template instantiation", 用以专注于特定的实体

    编译器的决议(resolution)算法必须决定哪一个才是适当的scope,然后在其中搜寻适当的name。

    Member Function的实例化行为(Member function instantiation)

    对于template的支持,最困难莫过于template function的具现(instantiation),目前的编译器提供了两个策略:一个是编译时期策略,程序代码必须在program text file中备妥可用;另一个是链接时期策略,程序代码必须在meta-compliation工具可以导引编译器的具现行为(instantiation)。

    下面是编译器设计者必须回答的三个主要问题:

    1. 编译器如何找出函数的定义?
      答案之一是包含template program text file,就好像它是个header文件一样,Borland编译器就是遵循这个策略。另一种方法是要求一个文件命名规则,例如,我们可以要求,在Point.h文件中发现的函数声明,其template program text一定要放置于文件Point.c或者Point.cpp中,以此类推。cfront就是遵循这个策略。Edison Desigin Group编译器对此两种策略都支持。
    2. 编译器如何能够只具现出程序中用到的member functions?
      解决办法之一就是,根本忽略这项要求,把一个已经具现出来的class的所有member functions都产生出来。Borland就是这么做的——虽然它也提供#pragmas让你压制(或具现出)特定实体。另一种策略就是仿真链接操作,检测看看哪一个函数真正需要,然后只为它(们)产生实体。cfront就是这么做的,Edison Design Group编译器对此两种策略都支持。
    3. 编译器如何阻止member definitions在多个 .o文件中都被具现呢?
      解决办法之一是产生多个实体,然后从链接器中提供支持,只留下其中一个实体,其余都忽略。另外一个办法就是由使用者来导引“仿真链接阶段”的具现策略,决定哪些实体(instances)才是所需求的。

    目前,不论是编译时期还是链接时期的实例化(instantiation)策略,均存在以下弱点:当template实例被产生出来时,有时候会大量增加编译时间。很显然,这将是template functions第一次实例化时的必要条件。然而当那些函数被非必要地再次实例化,或是当“决定那些函数是否需要再实例化”所花的代价太大时,编译器的表现令人失望

    C++支持template的原始意图可以想见是一个由使用者导引的自动实例化机制,既不需要使用者的介入,也不需要相同文件有多次的实例化行为。但是这已被证明是非常难以达成的任务,比任何人此刻所想象的还要难。

    异常处理(Exception Handing)

    欲支持exception handling,编译器的主要工作就是找出catch子句,以处理被丢出来的exception。这多少需要追踪程序堆栈中的每一个函数当前作用区域(包括追踪函数中的local class objects当时的情况)。同时,编译器必须提供某种查询exception objects的方法,以知道其实际类型(这直接导致某种形式的执行期识别,也就是RTTI)。最后,还需要某种机制用以管理被丢出的object,包括它的产生、储存、可能的解构(如果有相关的destructor)、清理(clean up)以及一般存取,也可能有一个以上的objects同时起作用。

    一般而言,exception handling机制需要与编译器所产生的数据结构以及执行期的一个exception library紧密合作,在程序大小和执行速度之间,编译器必须有所抉择:

    • 为了维持执行速度,编译器可以在编译时期建立起用于支持的数据结构,这会使程序大小膨胀,但编译器可以几乎忽略这些结构,直到有个exception被丢出来。
    • 为了维持程序大小,编译器可以在执行期建立起用于支持的数据结构。这会影响程序的执行速度,但意味着编译器只有在必要的时候才建立那些数据结构(并且可以抛弃之)。

    Exception Handling 快速检阅

    C++的exception handing由三个主要的语汇组件构成:

    1. 一个throw子句。它在程序某处发出一个exception。被抛出去的expection可以是內建类型,也可以是使用者自定类型。
    2. 一个或多个catch子句。每一个catch子句都是一个exception handler。它用来表示说,这个子句准备处理某种类型的exception,并且在封闭的大括号区段中提供实际的处理程序
    3. 一个try区段。它被围绕以一系列的叙述句(statements),这些叙述句可能会引发catch子句起作用

    当一个exception被丢出去时,控制权会从函数调用中被释放出来,并寻找一个吻合的catch子句。如果都没有吻合者,那么默认的处理例程terminate()会被调用。当控制权被抛弃后,堆栈中的每一个函数调用也就被推离(popped up),这个程序称为unwinding the stack。在每一个函数被推离堆栈之前,函数的local class objects的destructor会被调用。

    对Exception Handling的支持

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

    1. 检验发生throw操作的函数;
    2. 决定throw操场是否发生在try区段中;
    3. 若是,编译系统必须把exception type拿来和每一个catch子句比较;
    4. 如果比较吻合,流程控制应该交到catch子句手中;
    5. 如果throw的发生并不在try区段中,并没有一个catch子句吻合,那么系统必须(a)摧毁所有active local objects,(b)从堆栈中将当前的函数"unwind"掉,(c)进行到程序堆栈中的下一个函数中去,然后重复上述步骤2~5

    当一个实际对象在程序执行时被丢出,会发生什么事?
    当一个exception被丢出时,exception object会被产生出来并通常放置在相同形式的exception数据堆栈中,从throw端传染给catch子句的是exception object的地址、类型描述器(或是一个函数指针,该函数会传回与该exception type有关的类型描述器对象),以及可能会有的exception object描述器(如果有人定义它的话)。

    考虑一个catch子句如下:

    catch(exPoint p){
        //do something
        throw;
    }
    

    以及一个exception object,类型为exVertex,派生自exPoint。这两种类型都吻合,于是catch子句会作用起来。那么p会发生什么事?

    • p将以exception object作为初值,就像是一个函数参数一样。这意味着如果定义有(或由编译器合成出)一个copy constructor和一个destructor的话,它们都会实施于local copy身上。
    • 由于p是一个object而不是一个reference,当其内容被拷贝的时候,这个exception object的non-exPoint部分会被切掉(sliced off)。此外,如果为了exception的继承而提供有virtual functions,那么p的vptr会被设为exPoint的virtual table;exception object的vptr不会被拷贝。

    当这个exception被再丢出一次时,会发生什么事情呢?p是一个local object,在catch子句的末端将被摧毁。丢出p需得产生另一个临时对象,并意味着丧失原来的exception的exVertex部分。原来的exception object被再一次丢出,任何对p的修改都会被抛弃。

    像下面这样的一个catch子句:

    catch(exPoint &rp){
        //do something
        throw;
    }
    

    则是参考到真正的exception object。任何虚拟调用都会被决议(resolved)为instances active for exVertex,也就是exception object的真正类型。任何对此object的改变都会被复制到下一个catch子句中。

    执行期类型识别(Runtime Type Identification, RTTI)

    在cfront中,用以表现出一个程序所谓的“内部类型体系”,看起来像:

    //程序层次结构的根类 root class
    class node{ ... };
    
    //root of 'type' subtree: basic types,
    //'derived' types: points, arrays,
    //functions, classes, enums, ...
    class type : public node{ ... };
    
    //two representations for functions
    class fct : public type{ ... };
    class gen : public type{ ... };
    

    其中gen是generic的简写,用来表现一个overloaded function。

    于是只要你有一个变量,或是类型为type*的成员(并知道它代表一个函数),你就必须决定其特定的derived type是否为fct或是gen。

    Type-Safe Downcast(保证安全的向下转型操作)

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

    • 需要额外的空间以存储类型信息(type information),通常是一个指针,指向某个类型信息节点
    • 需要额外的时间以决定执行期的类型(runtime type),因为,正如其名所示,这需要再执行期才能决定

    冲突发生在两组使用者之间:

    1. 程序员大量使用多态(polymorphism),并因而需要正统而合法的大量downcast操作。
    2. 程序员使用内建数据类型以及非多态设备,因而不受各种额外负担所带来的报应。

    理想的解决方案是:为两派使用者提供正统而合法的需要——虽然或许得牺牲一些设计上的纯度与优雅性。

    C++的RTTI机制提供一个安全的downcast设备,但只对那些展现"多态(也就是使用继承和动态绑定)"的类型有效。我们如何分辨这些?编译器能否光看class的定义就决定这个class用以表现一个独立的ADT或是一个支持多态的可继承子类型(subtype)?当然,策略之一就是导入一个新的关键词,优点是可以清楚地识别出支持新特性的类型,缺点则是必须翻新旧程序。

    另一个策略是经由声明一个或多个virtual functions来区别class声明。其优点是透明化地将旧有程序转化过来,只要重新编译就好。缺点则是可能会将一个其实并非必要的virtual function强迫导入继承体系的base class身上。在C++中,一个具备多态性质的class(所谓的polymorphic class),正是内含继承而来(或是直接声明)的virtual functions。

    从编译器的角度来看,这个策略还有其他优点,就是大量降低额外负担。所有polymorphic classes的objects都维护了一个指针(vptr),指向virtual function table,只要我们把与该class相关的RTTI object地址放进virtual table中(通常放在第一个slot),那么额外负担就降低为:每一个class object只多花费一个指针。这个指针只需被设定一次,它是被编译器静态设定,而不是在执行期由class constructor设定(vptr才是这么设定)。

    Type-Safe Dynamic cast(保证安全的动态转型)

    dynamic_cast运算符可以在执行期决定真正的类型。如果downcast是安全的(也就是说,如果base type pointer指向一个derived class object),这个运算符会传回被适当转换过的指针。如果downcast不是安全地,这个运算符会传回0

    References并不是Pointers

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

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

    dynamic_cast运算符也适用于reference身上。然而对于一个non-type-safe cast,其结果不会与施行于指针的情况相同。为什么?一个reference不可以像指针那样"把自己设为0就代表了 no object";若将一个reference设为0,会引起一个临时性对象(拥有被参考到的类型)被产生出来,该临时对象的初值为0,这个reference然后被设定成为该临时性对象的一个别名(alias)。

    因此当dynamic_cast运算符施行于一个reference时,不能够提供对等于指针情况下的那一组true/false。取而代之的是,会发生下列事情:

    • 如果reference真正参考到适当的derived class(包括下一层或下下一层或下下下一层或...),downcast会被执行而程序可以继续执行。
    • 如果reference并不真正是某一种derived class,那么,由于不能传回0,遂丢出一个bad_cast exception.

    Typeid运算符

    typeid运算符传回一个const reference,类型为type_info。

    type_info object由什么组成? C++ Standard中对type_info的定义如下:

    class type_info{
    public:
        virtual ~type_info();
        bool operator==(const type_info& ) const;
        bool operator!=(const type_info& ) const;
        bool before(const type_info&) const;
        bool char* name() const;  //传回class原始名称
    private:
        //prevent memberwise init and copy
        type_info(const type_info& );
        type_info& operator=(const type_info& );
        //data members
    };
    

    编译器必须提供的最小量信息是class的真实名称、以及在type_info objects之间的某些排序算法(这就是before()函数目的)、以及某些形式的描述器,用以表现explicit class type和这个class的任何subtype。

    虽然RTTI提供的type_info对于exception handling的支持来说是必要的,但对于exception handling的完整支持而言,还不够。如果再加上额外一些type_info derived classes,就可以在exception发生时提供有关于指针、函数及类等等的更详细信息。

  • 相关阅读:
    [Json.net]忽略不需要的字段
    [c#]exchange回复,全部回复,转发所遇到的问题
    [c#]获取exchange中的图片
    [c#基础]AutoResetEvent
    [C#基础]c#中的BeginInvoke和EndEndInvoke
    [CentOs7]安装mysql
    [CentOS7]安装mysql遇到的问题
    [CentOs7]图形界面
    [CentOS]添加删除用户
    在虚机中安装CentOS
  • 原文地址:https://www.cnblogs.com/lengender-12/p/7003450.html
Copyright © 2011-2022 走看看