0%

第五章 构造、析构、拷贝语意学

纯虚函数的存在(Presence of a Pure Virtal Function)
  • 考虑abstract base class声明:
    1
    2
    3
    4
    5
    6
    7
    8
    class Abstract_base {
    public:
    virtual ~Abstract_base() = 0;
    virtual void interface() const = 0;
    virtual const char* mumble() const { return _mumble; }
    protected:
    char *_mumble;
    };
    这个class被设计为一个抽象的base class(其中有pure virtual function,使得Abstract_base不可能有实例),但需要一个显式的构造函数以初始化data member _mumble。如果没有初始化操作,derived class的局部性对象_mumble及那个无法决定初值。
纯虚函数的存在(Presence of a Pure Virtual Function)
  • pure virtual function只能被静态地调用invoked statically),不能经由虚拟机制调用。而pure virtual destructor,class设计者一定得定义它。因为每个derived class class以及base class destructor。只要缺乏定义,就会导致链接失败
虚拟规格的存在(Presence of a Virtual Specification)
  • 如果决定把Abstract_base::mumble()设计为virtual function,那是糟糕的选择,因为其函数定义内容并不与类型有关,因而几乎不会被后继的derived class改写。
虚拟规格中const的存在
  • 决定要给virtual function是否需要const,是一件繁琐的事情。如果声明为const,意味着subclass实例可能被无穷次数地使用。不把函数声明为const,意味和函数不能获得一个const reference或const pointer。头大的是,声明函数为const,才发现derived instance必须修改某一data member。解决办法就是不再用const就是了。
重新考虑class的声明
  • 综上所述,这才是比较适当的一种设计:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Abstract_base {
    public:
    virtual ~Abstract_base(); // nonpure virtual
    virtual void interface() = 0; // nonconst
    const char* mumble() const { return _mumble; } // nonvirtual
    protected:
    Abstract_base(char *pc =ss 0);
    char *_mumble;
    }

5.1 ”无继承”情况下的对象构造

  • 考虑程序片段:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Point global;                   //1
    //2
    Point foobar() //3
    { //4
    Point local; //5
    Point *heap = new Point; //6
    *heap = local; //7
    // ... stuff ... //8
    delete heap; //9
    return local; //10
    } //11

    L1、L5、L6分别为:global内存配置local内存配置heap内存配置。一个object的声明,是该object的一个执行期属性。local object的生命从L5定义开始,L10为止。global object的生命和这个程序的生命相同。heap object的生命从被new运算符配置出来开始,到delete运算符摧毁结束。
    下面是Point的第一次声明,写成C程序。这是一种Plain OI’ Data声明形式:

    1
    2
    3
    4
    5
    typedef struct
    {
    float x, y, z;
    Point;
    }

    如果用C++来编译这代码,编译器会为Point声明一个trivial default constructor、一个trivial destructor、一个trivial copy constructor,以及一个trivial copy assignment operator。但实际上,编译器会分析这个声明,并贴上POD标签

  • 当编译器遇到这样的定义:

    1
    Point global;                   //1

    观念上Point的trival constructor和destructor会被产生并调用,constructor在程序起始(startup)被调用,而destructor在程序的exit()处被调用。然而事实上trivial members要么没定义,要么没被调用。
    在C中,global被视为一个临时性的定义,因为它没有显式的初始化操作。它可以在程序中发生多次。实例会被折叠起来,只留下一个,放在程序data segment中。这块空间称为BSS,这是Block Started by Symbol缩写。
    C++并不支持临时性定义。虽然它可以判断class object或是POD。global在C++中被视为完全定义(会阻止多个定义)。C和C++的差异是,BSS data segment在C++中相对不重要。所有的全局变量都被以“初始化过的数据”来对待
    至于foobar()函数的L5,既没有构造也没有被析构,不过可能没有经过初始化就会成BUG(L7)。至于heap object:

    1
    2
    3
    4
    5
    Point *heap = new Point;    //6
    // 转化
    Point *heap = __new(sizeof(Point));
    // 有初始化过就没问题
    *heap = local; //7

    事实上L7会产生编译警告,观念上,这样的指定操作会触发trivial copy assignment operator做拷贝搬运操作。而实际上object是POD,所以assignment只是像C那样纯粹bitwise搬运。

抽象数据类型(Abstract Data Type)
  • 以下是Point声明,提供了完整封装性,但没有提供virtual function:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Point {
    public:
    Point(float x = 0.0, float y = 0.0, float z = 0.0) : _x(x), _y(y), _z(z) {}
    // no copy constructor, copy operator
    // or destructor defined ...
    // ...
    private:
    float _x, _y, _z;
    };
    这里还是三个连续的float,不论private或public存取层或是member function都不会占用额外的对象空间。没有定义copy constructor或copy operator,因为有default bitwise semantics足够了,也不需要destructor。对于实例:
    1
    Point global; // Point::Point(0.0, o.0, 0.0);
    有了default constructor,global被定义在全局范畴,初始化操作将延迟到程序启动(startup)。
    如果要将class中的所有成员设定常量初值,给予一个explicit initialization list会比较有效率(相比constructor的inline expansion而言)。但它有三项缺点:
    1. 只有当class members都是public,才会奏效。
    2. 只能指定常量,因为在编译器就能评估求值。
    3. 由于编译器并不自动施行,初始化行为可能性很高。
为继承做准备
  • 第三个Point声明,为继承性质和某些操作的动态决议dynamic resolution)做准备,目前限制z成员做存取操作:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Point {
    public:
    Point(float x = 0.0, float y = 0.0) : _x(x), _y(y) {}
    // no destructor, copy constructor, or
    // copy operator defined ...
    virtual float z();
    // ...
    protected:
    float _x, _y;
    };
    virtual functions的导入在这里附带一个virtual destructor的声明在这个例子里,并无好处。除此之外,每个class object多负担一个vptr之外,virtual function的导入也引发编译器对Point class产生的膨胀作用:
    • 定义的constructor附带了一些代码,以便vptr初始化
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      // C++伪码
      Point* Point::Point(Point *this, float x, float y) : _x(x), _y(y)
      {
      // 设定object的vptr
      this->__vptr_Point = __vtbl__Point;
      // 扩展member initialization list
      this->_x = x;
      this->_y = y;
      // 传回this
      return this;
      }
    • 合成一个copy constructor和copy assignment operator,而且操作不是trivial(implicit destructor仍然是)。如果Point object被初始化或以derived class object赋值,那么以为基础(bitwise)的操作可能对vptr带来非法设定:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      // C++伪码
      // copy constructor 内部合成
      inline Point* Point::Point(Point*this, const Point &rhs)
      {
      // object的vptr
      this->__vptr_Point = __vtbl__Point;
      // copy
      return this;
      }
      编译器在优化状态会把object内容连续拷贝到另一个object,而不是精准memberwise。L7的memberwise赋值操作可能出发copy assignment operator的合成,及调用inline expansion(行内扩张):以this取代heap,以rhs取代local
      戏剧性的冲击在L10。由于copy constructor的出现,foobar()可能转化为:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      Point foobar(Point &__result)
      {
      Point local;
      local.Point::Point(0.0, 0.0);
      // heap
      // copy constructor
      __result.Point::Point(local);
      // local destructor
      // Point::derstructor
      // local.Point::~Point();
      return
      }
      如果NRV优化,转化为:
      1
      2
      3
      4
      5
      6
      Point foobar(Point &__result)
      {
      __result.Point::Point(0.0, 0.0);
      // heap
      return;
      }

5.2 继承体系下的对象构造

  • 当定义一个object:

    1
    T object;

    如果T有constructor,它会被调用。除此之外,constructor调用伴随了什么?constructor可能内含大量隐藏码,因为编译器会扩充每个constructor

    1. 在member initialization list中的data members初始化操作会被放进constructor函数本体,以声明顺序为顺序。
    2. 如果member没有在member initialization list中,但这个member有default constructor,那么被调用。
    3. 在那之前,如果class object有vptr,必须被设定初值指向适当vtbls。
    4. 在那之前,base class constructors必须被调用,以base class声明顺序为顺序。
      • 如果base class被列入member initialization list中,那么任何显式参数都传递进去
      • 如果base class没有被列入member initialization list中,而它有default constructor(或default memberwie copy constructor),那么调用它
      • 如果base class是多层继承下的第二或后继base class,那么调整this指针
    5. 在那之前,所有virtual base class constructors必须被调用,从左到右,从深到浅:
      • 如果class被列入member initialization list中,那么任何显式指定的参数,都传递进去。如果没有列入list中,而class有default constructor,调用它
      • class中的每个virtual base class subobject的偏移地址(offset)必须在执行期可被存取
      • 如果class object是最底层(most-derived)的class,其constructors可能被调用;用以支持者行为的机制必须放出来
  • 以Point为例,探讨constructors扩充的必要性:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class Point {
    public:
    Point(float x = 0.0, float y = 0.0);
    Point(const Point&); // copy constructor
    Point& operator=(const Point&); // copy assignment operator
    virtual ~Point(); // virtual derstructor
    virtual float z() { return 0.0; }
    // ...
    protected:
    float _x, _y;
    };

    class Line {
    Point _begin, _end;
    public:
    Line(float = 0.0, float = 0.0, float = 0.0, float = 0.0);
    Line(const Point&, const Point&);
    draw();
    // ...
    };

    。。。。。。。。

虚拟继承(Virtual Inheritance)
  • 考虑下面虚拟继承,继承自Point:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Point3d : public virtual Point {
    public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0) : Point(x, y), _z(z) {}
    Point3d(const Point3d &rhs) : Point(rhs), _z(rhs._z) {}
    ~Point3();
    Point3d& operator=(const Point3d&);
    virtual float z() { return _z; }
    // ...
    protected:
    float _z;
    };
    传统的constructor扩充并没有用,因为virtual base class的共享性之故:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 不合法
    Point3d* Point3d::Point3d(Point3d *this, float x, float y, float z)
    {
    this->Point::Point(x, y);
    this->__vptr_Piont3d = __vtbl_Point3d;
    this->__vptr_Point3d__Point = __vtbl_Point3d_Point;
    this->_z = rhs._z;
    return this;
    }
    看出什么错误了吗?
    1
    2
    3
    4
    5
    6
    graph BT
    Point3d -- virtual --- Point
    Vertex -- virtual --- Point
    Vertex3d -- public --- Point3d
    Vertex3d -- public --- Vertex
    pVertex -- public --- Vertex3d
    Vertex的constructor必须调用Point constrcutor。然而当Point3d和Vertex为Vertex3d的subobjects时,则调用操作不一定发生。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // Point3d的constructor扩充内容
    Point3d* Point3d::Point3d(Point3d *this, bool __most_derived, float x, float y, float z)
    {
    if (__most_derived != false)
    this->Point::Point(x, y);
    this->__vptr_Point3d = __vtbl_Point3d;
    this->__vptr_Point3d__Point = __vtbl_Point3d__Point;
    this->_Z = ths._z;
    return this;
    }
    Vertex3d调用Point3d和Vertex的constructor时,会把__most_derived参数设为false,于是就压制了对两个constructors中对Point constructor的调用操作。
    1
    2
    3
    4
    5
    6
    7
    8
    Vertex3d* Vertex3d::Vertex3d(Vertex3d *this, bool __most_derived, float x, float y, float z)
    {
    if (__most_derived != false)
    this->Point::Point(x, y);
    this->Point3d::Point3d(false, x, y, z);
    this->Vertex::Vertex(false, x, y);
    return this;
    }
    这个策略得以保持语义正确。当定义一个Point3d时,Point3d constructor可以正确调用Point virtual base class subobject。当定义Vertex3d时时,Vertex3d constructor正确调用Point constructor。Point3d和Vertex的constructor不会对Point调用。这种把一个constructor分裂为二的做法,可以带来速度的提升。??
vptr初始化语意学(The Semantics of the vptr Initialization)
  • 当我们定义PVertex object时,constructors的调用顺序是:
    1
    2
    3
    4
    5
    Point(x, y);
    Point3d(x, y, z);
    Vertex(x, y, z);
    Vertex3d(x, y, z);
    PVertex(x, y, z);
  • 假设继承体系中每个class都定义一个virtual function size(),返回class的大小。而这个继承体系中的每个constructors内含调用操作:
    1
    2
    3
    4
    5
    Point3d::Point3d(float x, float y, float z) : _x(x), _y(y), _z(z)
    {
    if (spyOn)
    cerr << "Within Point3d::Piont3d()" << "size: " << size() << endl;
    }
    在一个class的constructor(和destructor)中,经由构造中的对象来调用virtual function,其函数实例是在此class中有作用的那个。如果调用操作限制必须在constructor(或destructor)中直接调用,那么答案十分明显;将每个调用以静态方式决议它,不要用到虚拟机制。如果是在Point3d constructor中,就显式调用Point3d::size()。
  • 如果size()之中又调用一个virtual function,这个调用也必须决议为Point3d的函数实例。因此在执行constructor时,必须限制一组virtual functions候选名单。为了控制class中有所作用的函数,编译系统需要控制住vptr的初始化和设定操作,vptr初始化操作怎么处理,得视vptr在constructor中应该在什么时候被初始化而定。 在base class constructors调用操作之后,在member iniaialization list中members初始化操作前。它解决了class中限制一组virtual functions名单的问题。如果每个constructor都一直等待其base class constructors执行完毕之后才设定对象的vptr,那么每次都能调用正确的virtual function实例。
    constructor的执行算法通常如下:
    1. 在derived class constructor中,所有virtual base classes及上层base class的constructors会被调用
    2. **对象的vptr(s)会被初始化,指向相关virtual table(s)**。
    3. 如果有member initialization list,将在constructor内扩展开来。这在vptr被设定之后才做,以免virtual member function被调用。
    4. 最后才执行程序员所提供的代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      PVertex::PVertex(float x, float y, float z) : _next(0), Vertex3d(x, y, z), Point(x, y)
      {
      if (spyOn)
      cerr << "Within PVertex::PVertex()" << "size: " << size() << endl;
      }
      // 扩展
      // C++伪码
      PVertex* PVertex::PVertex(PVertex *this, bool __most__derived, float x, float y, float z)
      {
      // 条件式地调用virtual base constructor
      if (__most__derived != false)
      this->Point::Point(x, y);

      // 无条件调用上一层base
      this->Vertex3d::Vertex3d(x, y, z);

      // 将相关的vptr初始化
      this->__vptr_PVertex = __vtbl_PVertex;
      this->__vptr_Point__PVertex = __vtbl_Point__PVertex;

      // 程序员缩写的代码
      if (spyOn)
      cerr << "Within PVertex::PVertex()"
      << "size: "
      // 经由虚拟机制调用
      << (*this->__vptr__PVertex[3].faddr)(this)
      << endl;
      // 传回被构造的对象
      return this;
      }
  • 下面是vptr必须被设定的两种情况:
    1. 当一个完整的对象被构造起来时。如果我们声明一个Point对象,则Point constructor必须设定其vptr。
    2. 当一个subobject constructor调用一个virtual function(不论是直接调用还是间接调用)时。
      p218

5.3 对象复制语意学(Object Copy Semantics)

  • 当我们设计一个class,并以一个class object指定给另一个class object时,我们有三种选择

    1. 什么都不做,施行默认行为
    2. 提供一个explicit copy assignment operator
    3. 显式地拒绝把一个class object指定给另一个class object

    当选择不准将一个class object指定给另一个class object时,只要把copy assignment operator声明为priavte,并且不提供其定义就可以了。设为private,就不再允许于任何地点(除了在member functions以及该class的friends之中)做赋值assign)操作。

  • 需要验证copy assignment operator的语意,利用Point class来帮助讨论:

    1
    2
    3
    4
    5
    6
    7
    class Piont {
    public:
    Point(float x = 0.0, float y = 0.0);
    // ... 没有virtual function
    protected:
    float _x, _y;
    };

    对于默认行为是否足够,如果要支持的只是一个简单的拷贝操作,那么默认行为不但足够而且有效率,而且没有理由再自己提供一个copy assignment operator。
    如果不对Point供应一个copy assignment operator,光是以来memberwise copy,编译器不会产生实例。编译器不会产生出一个实例,因为class有了bitwise copy语义,所以implicit copy assignment operator被视为毫无用处,也不会被合成出来。copy assignment operators并不表示bitwisecopy semantics是nontrivial,只有nontrivial instances才会被合成出来。因此,对于Point class,这样的赋值操作:

    1
    2
    3
    Point a, b;
    ...
    a = b;

    由bitwise copy完成,期间并没有copy assignment oeprator被调用。注意,我们还是可能提供一个copy constructor,为的是把name return valueNRV)优化打开。copy constructor的出现不意味着也也要提供一个copy assignment operator。

  • 如果该operator在继承之下呢?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    inline Point& Point::operator=(const Point &p)
    {
    _x = p._x;
    _y = p._y;
    return *this;
    }
    // 虚拟继承
    class Point3d : virtual public Point {
    public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0);
    // ...
    protected:
    float _z;
    };
    // 如果没有定义copy assignmnet operator,根据合成规则的第二项和第四项。合成的东西看起来这样:
    inline Point3d& Point3d::operator=(Point3d *const this, const Piont3d &p)
    {
    // call base class
    this->Point::operator=(p);

    // memberwise copy the derived class members
    _z = p._z;
    return *this;
    }

    它缺乏一个member assignment list(平行于member initialization list的东西)。缺少copy assignment list,看起来是小事,但如果没有它,编译器一般就没有办法压抑上一层base class的copy operators被调用。例如Vertex copy operator,Vertex虚拟继承自Point:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // class Vertex : virtual public Point
    inline Vertex& Vertex::operator=(const Vertex &v)
    {
    this->Point::operator=(v);
    _next = v._next;
    return *this;
    }
    // 从Point3d和Vertex派生Vertex3d,下面是其copy assignment operator
    inline Vertex3d& Vertex3d::operator=(const Vertex3d &v)
    {
    this->Point::operator=(v);
    this->Point3d::operator=(v);
    this->Vertex::oeprator=(v);
    ...
    }

    编译器怎么能在Point3d和Vertex的copy assignment operators中压抑Point的copy assignment operators呢?有一种方法可以保证most-derived class会(完成)virtual base class subobject的copy行为,就是在derived class的copy assignment operator函数实例的最后,显式调用oeprator:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    inline Vertex3d& Vertex3d::operator=(const Vertex3d &v)
    {
    this->Point3d::oeprator=(v);
    this->Vertex::oeprator=(v);
    // must place this last if your compiler does
    // not suppress intermediate class invocations
    this->Point::operator=(v);
    ...
    }

    它不能够省略subobjects的多重拷贝,但可以保证语意。建议尽可能不要允许一个virtual base class的拷贝操作,甚至一个比较奇怪的建议:不要在任何virtual base class中声明数据。

5.4 对象的效能(Object Efficiency)

  • 测试。

5.5 析构语意学(Semantics of Destruction)

  • 如果class没有定义destructor,那么只有在class内含的member object(抑或是class的base class)拥有destructor的情况下,编译器才会自动合成一个来。当从父类派生子类(即使是一种虚拟派生关系)时,如果没有声明一个estructor,编译器就没有必要合成一个destructor。没有任何理由说在delete一个对象之前得先讲内容清除干净。在一个对象的生命之前,没有任何class使用层面的程序操作是必要的,因此也就不需要要给destructor。

  • 一个由程序员定义的destructor被扩展的方式类似constructors被扩展的方式,但顺序相反:

    1. destructor的函数本体现在被执行,也就是说vptr会在程序员的代码执行前被重设(reset)
    2. 如果class拥有member classs objects,而后者会拥有destructors,那么它们会以生命顺序的相反顺序被调用
    3. 如果object内含一个vptr,那么首先重设(reset)相关的vtbl。指向适当的base class的vtbl
    4. 如果由任何直接的(上一层)nonvirtual base classes拥有destrucotr,它们会以其声明顺序的相反顺序被调用
    5. 如果有任何virtual base classes拥有destructor,那么也会以原来的构造顺序的相反顺序被调用
  • 跟constructor一样,对destructor的一种最佳实现策略就是维护两份destrucotr实例:

    1. 一个complete object实例,先设定好vptr(s),并调用virtual base classes destructors。
    2. 一个base class subobject实例:除非在destructor函数中调用一个virtual function,否则不会调用virtual base class destructors并设定vptr。
  • 一个object的声明结束于其destructor开始执行之时