0%

第二章 构造函数语义学

  • 关键词explicit之所以被导入这个语言,就是为了给程序员一个方法,使他们能够制止“单一参数的constructor”被当作conversion运算符。

2.1 Default Constructor的构造操作

“带有Default Constructor”的Member Class Object
  • 如果class没有constructor,但有一个member object,而且这个object有default constructor,那么这个class的implicit default constructor是nontrivial的。举个例子,下面程序片段中,编译器为class Bar合成一个default constructor:
    1
    2
    3
    4
    5
    6
    7
    class Foo { public: Foo(), Foo(int) ... };
    class Bar { public: Foo foo; char *str; }
    void foo_bar()
    {
    Bar bar; // Bar::foo 必须在此处初始化。
    if (str) { } ...
    }
    被合成的Bar default constructor内含代码,能够调用class Foo的default constructor处理member object Bar::foo,因为Bar::foo初始化是编译期的责任:
    1
    2
    3
    4
    5
    // 为member foo调用class Foo的default construcotr
    inline Bar::Bar()
    {
    foo.Foo::Foo();
    }
    而且,被合成default constructor只满足编译期需求,并不是程序员需求。如果default constructor由程序员显式定义出来了,那么编译器的行动是:
  • 如果class A内含一个或一个以上的member class objects,那么class A的每一个constructor必须调用每一个member classes的default constructor。编译器会扩张已存在的constructors,使得user code被执行之前,先调用的default constructor。
“带有Default Constructor”的Base Class
  • 如果class没有constructors,却派生自有default constructor的base class,那么这个derived class的default constructor会被视为nontrivial。
  • 如果提供多个constructor,但都没有default constructor,编译器会扩张现有的每一个constructors,将用以调用必要default constructors的程序代码加进去。它不会合成一个新的default constructor。
“带有一个Virtual Function”的Class
  • 另外有两种情况,也要合成default constructor:
    1. class声明(或继承)一个virtual function
    2. class派生自一个继承串链,其中有一个或更多的virtual base classes。
      1
      2
      3
      graph BT
      Bell --- Widget
      Whistle --- Widget
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      class Widget {
      public:
      virtual void flip() = 0;
      // ...
      };
      void flip(const Widget &widget) { Widget.flip(); }
      void foo()
      {
      Bell b;
      Whistle w;
      flip(b);
      flip(w);
      }
      两个扩张在编译期间发生了:
    3. 一个virtual function table(vtbl)被产生出来,内含class的virtual functions地址。
    4. 每个class object中,额外的pointer member(vptr)会被合成出来,内含相关的class vtbl地址。
    widget.flip()虚拟调用操作会被重新改写,以使用widget之中的vtpr和vtbl:
    1
    { *widget.vptr[1])(&widget)
“带有一个Virtual Base Class”的Class
  • 看以下代码:
    1
    2
    3
    4
    5
    graph BT
    A --- X
    B --- X
    C --- A
    C --- B
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class X { public: int i; } ;
    class A : public virtual X { public: int j; };
    class B : public virtual X { public: double d; };
    class C : public A, public B { public: int k; };
    // 无法在编译时期决定(resolve)pa->X::i的位置
    void foo(const A *pa) { pa->i = 1024; }
    main()
    {
    foo(new A);
    foo(new C);
    // ...
    }
    因为pa的类型可以改变,编译器无法固定住foo()中经由pa而存取的X::i的实际偏移位置。编译器必须改变“执行存取操作”的那些代码,使X::i可以延迟至执行期才决定下来。cfont做法是在virtual base classes中安插指针完成。经由reference或pointer存取virtual base class的操作都可以通过指针完成。
    1
    void foo(const A *pa) {pa->__vbcX->i = 1024; }
    __vbcX实在class object构造期间被完成的。编译器会安插允许每个virtual base class在执行器存取操作的代码,如果base没有声明任何constructor,编译器必须为它合成一个default的。

2.2 Copy Constructor的构造操作

  • 有三种情况,会以一个obect的内容作为另一个class object的初值。
    • 对object做显式初始化操作
    • 当object作为参数交给某个函数时
    • 函数回传一个class object时
Default Memberwise Initialization
  • 如果class没有提供explicit copy constructor,其内部是以default memberwise initialization手法完成的,就是把每个内建的或派生的data member(如指针或数组)的值,从object拷贝到另一个object上,不过它并不会拷贝其中的member class object。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    class String {
    public:
    // ... 没有explicit copy constructor
    private:
    char *str;
    int len;
    };

    // String object的default memberwise initialization发生在这种情况下:
    String noun("book");
    String verb = noun;

    // 完成方式好像个别设定每个members一样
    verb.str = noun.str;
    verb.len = noun.len;
    如果String object被声明为一个class的member:
    1
    2
    3
    4
    5
    6
    7
    class Word {
    public:
    // ... 没有explicit copy constructor
    private:
    int _occurs;
    String _word;
    };s
    那么Word object的default memberwise initialization会拷贝_occurs,然后再于_word身上递归实施memberwise initialization。
  • 一个class可用两种方式复制得到:
    • 一是被初始化。以copy constructor完成。
    • 二是被指定(asignment)。以copy assignment operator完成。
Bitwise Copy Semantics(位逐次拷贝)
  • 一个class没有定义explicit copy constructor,是否有编译器合成实例,取决于class是否展现bitwise copy semantics而定。有两个例子:
    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
    31
    // 声明展现了bitwise copy semantics
    // 这种情况下不需要合成default copy constructor
    class Word {
    public:
    Word(const char*);
    ~Word() { delete []str; }
    // ...
    private:
    int cnt;
    char *str;
    };

    // 以下声明未展现出bitwise copy semantics
    // 这种情况下需要合成出一个copy constructor,以便调用member class String object的copy constructor
    class Word {
    public:
    Word(const String&);
    ~Word();
    // ...
    private:
    int cnt;
    String str;
    };
    // 其中String声明了explicit copy constructor
    class String {
    public:
    String(const char*);
    String(const String&);
    ~String();
    // ...
    };s
不要Bitwise Copy Semantics!
  • 有4中情况下,class不展现出“bitwise copy semantics”
    • 当class内有个member object,而该object声明有copy constructor时
    • 当class继承自一个base class,而该class存在copy constructor时,不论是显式声明的还是被合成的;
    • 当class声明了一个或多个virtual functional时
    • 当class派生自一个继承串联,其中有一个或多个virutal base clases时
重新设定Virtual Table的指针
  • 只要有一个class声明了一个或多个virtual functional,就会:
    • 增加vtbl,内含virtual function的地址
    • 一个指向vtbl的vptr,插在class object内
    当vptr导入到class之中,class就不展示biwise semantics了。一个新产生的class object的vptr不能成功而正确地设好初值会导致可怕的后果,编译器需要合成出一个copy constructor以求将vptr初始化。
    1
    2
    graph TD
    ZooAnimal --- Bear
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class ZooAnimal {
    public:
    ZooAnimal();
    virtual ~ZooAnimal();
    virtual void animate();
    virtual void draw();
    // ...
    private:
    // ...
    };

    class Bear : public ZooAnimal {
    public:
    Bear();
    void animate();
    void draw();
    virtual void dance();
    // ...
    private:
    // ...
    }
    ZooAnimal class object以另一个ZooAnimal class object作为初值,或Bear class object以另一个Bear class object作为初值,都可以直接靠bitwise copy semantics完成(除了member pointer)。
    1
    2
    Bear yogi;
    Bear winnie = yogi;
    这个例子里,yogi会被default Bear constructor初始化。yogi的vptr被指向Bear的vtbl(靠安插)。
    1
    ZooAnimal franny = yogi;    // sliced
    frany的vptr不可以被指向Bear的vtbl(如果yogi的vptr被bitwise copy,会导致此结果),否则当draw()被调用而franny被传进去时,会blow up:
    1
    2
    3
    4
    5
    6
    7
    8
    void draw(const ZooAnimal &zoey) { zoey.draw(); }
    void foo()
    {
    // franny的vptr指向ZooAnimal的vtbl
    ZooAnimal franny = yogi;
    draw(yogi); // call Bear::draw()
    draw(franny); // call ZooAnimal::draw()
    }
    通过franny调用virtual function draw(),调用的时ZooAnimal而非Bear实例。事实上,yogi中的Bear部分在franny初始化时被sliced掉了,只有franny声明为reference或pointer时才会是Bear的函数实例。
处理Virtual Base Class Subobject
  • 编译器必须让derived class object中的virtual base class subobject位置在执行期准备妥当。Bitwise copy semantics可能会破坏位置的完整性,所以编译器必须必须在它自己合成出来的copy constructor中做出仲裁:
    1
    2
    3
    4
    graph BT
    Bear -- public --- ZooAnimal
    Raccoon -- public virtual --- ZooAnimal
    RedPanda -- public --- Raccoon
    1
    2
    3
    4
    5
    6
    7
    8
    class Raccoon : public virtual ZooAnimal {
    public:
    Raccoon() { /*设定private data初值*/ }
    Raccoon(int val) { /*设定private data初值*/ }
    // ...
    private:
    // ...
    };
    一个virtual base class的存在会使bitwise copy semantics无效。问题不在于一个class object以另一个同类的object作为初值之时,而是发生于一个class object以其derived classes的某个object作为初值之时。
    一个Raccoon object作为另一个Raccoon object的初值,bitwise copy绰绰有余,而如果企图以RedPanda object作为little_critter的初值,编译器必须判断当后续企图存取ZooAnimal subobject时是否能正确执行:
    1
    2
    3
    // 简单的bitwise copy还不够,必须显式将little_critter的virtual base class pointer/offset初始化
    RedPanda little_red;
    Raccoon little_critter = little_red;
    在这种情况下,为了完成正确的little_critter初值设定,编译器合成一个copy constructor,安插代码设定virtual base class pointer/offset的初值(或是简单的确定它没被抹消),对每个members执行memberwise初始化操作,一起执行其他内存相关工作。

2.3 程序转化语义学(Program Transformation Semantics)

显式的初始化操作(Explicit Initialization)
  • 已知有定义:

    1
    X x0;

    下面三个定义,每个都以x0来初始化class object:

    1
    2
    3
    4
    5
    6
    7
    void foo_bar() 
    {
    X x1(x0);
    X x2 = x0;
    X x3 = X(x0);
    // ...
    }

    以上必要的程序转化有两个阶段

    • 重写每个定义,其中初始化操作被剥除
    • class的copy constructor调用操作被安插进去

    foo_bar()可能看起来这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    C++ 伪码
    void foo_bar()
    {
    // 定义重写,初始化操作被剥除
    X x1;
    X x2;
    X x3;
    // 编译器安插copy construction的调用操作
    // 表现出X::X(const X &xx);
    x1.X::X(x0);
    x2.X::X(x0);
    x3.X::X(x0);
    // ...
    }
参数的初始化(Argument Initialization)
  • C++ Standard说,把class object当作参数传给函数,或者作为函数的返回值,相当于初始化操作:
    1
    X xx = arg;
    这里xx是形式参数(或返回值),arg是真实参数。因此,函数:
    1
    2
    3
    4
    void foo(X x0);
    X xx;
    // ...
    foo(xx);
    会要求局部实例local instance)x0以memberwise方式将xx当初值。代码转换为:
    1
    2
    3
    4
    5
    6
    // C++伪码
    // 编译器产生的临时对象
    X __temp0;
    // 调用copy constructor
    __temp0.X::X(xx);
    foo(__temp0);
返回值的初始化(Return Value Initialization)
  • 已知下面函数的定义:
    1
    2
    3
    4
    5
    6
    X bar()
    {
    X xx;
    // ...
    return xx;
    }
    bar()的返回值如何从局部对象xx中拷贝过来?在cfont中的做法是一个双阶段转化:
    • 加上额外参数,类型是class object的reference。这个参数用来放置被拷贝构建copy constructed)而来的返回值。
    • return之前安插一个copy constructor调用
    bar()转换如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void bar(X &__result)
    {
    X xx;
    // default constructor
    xx.X::X();
    // ...
    // copy constructor
    __result.X:XX(xx);
    return; // 即使不传回任何值
    }
    1
    2
    3
    4
    X xx = bar();
    // 不用default constructor,NRV
    X xx;
    bar(xx);
    1
    2
    3
    4
    bar().memfunc();
    // 执行bar()所传回的X class object的memfunc()可能转换
    X __temp0;
    (bar(__temp0), __temp0).memfunc();
    1
    2
    3
    4
    5
    6
    // 同理,声明一个函数指针
    X (*pf)();
    pf = bar;
    // 转为
    void (*pf)(X&);
    pf = bar;
在使用者层面做优化(Optimization at the User Level)
  • 以下代码:
    1
    2
    3
    4
    5
    6
    7
    X bar(const T &y, const T &z)
    {
    X xx;
    // ...以y
    来处理xx
    return xx;
    }
    可以这么写:
    1
    2
    3
    4
    X bar(const T &y, const T &z)
    {
    return X(y, z);
    }
    当bar()被转换后,效率会比较高:
    1
    2
    3
    4
    5
    6
    // C++伪码
    void bar(X &__result)
    {
    __result.X::X(y, z);
    return;
    }
在编译器层面做优化(Optimization at the Compiler Level)
  • 在一个像bar()这样的函数中,所有return指令传回相同具名数值(named value),因此编译器可能自己做优化
    1
    2
    3
    4
    5
    6
    X bar()
    {
    X xx;
    // ...
    return xx;
    }
    xx以_result取代:
    1
    2
    3
    4
    5
    6
    7
    void bar(X &__result)
    {
    // default constructor被调用
    __result.X::X();
    // ... 直接处理__result
    return;
    }
    这样的优化操作,称为Named Return ValueNRV)。虽然NRV优化提供了重要效率改善,但它还是饱受批评。
    • 优化是默默的,不透明
    • 一旦函数复杂,很难优化
    • 某些人不喜欢
    举个例子,以下三个初始化语义上相等:
    1
    2
    3
    X xx0(1024);    // xx0.X::X(1024);
    X xx1 = X(1024);
    X xx2 = (X)1024;
    xx0是被单一的constructor操作设定初值:
    1
    xx0.X::X(1024);
    而xx1或xx2却调用两个constructor,产生临时性object,并针对临时object调用classX的destructor:
    1
    2
    3
    4
    X __temp0;
    __temp0.X::X(1024);
    xx1.X::X(__temp0);
    __temp0.X::~X();
Copy Constructor:要还是不要?
  • 一个3D坐标点类:
    1
    2
    3
    4
    5
    6
    7
    class Point3d {
    public:
    Point3d(float x, float y, float z);
    // ...
    private:
    float _x, _y, _z;
    };
    class的default copy constructor被视为trivial。默认情况下,一个Point3d class object的“memberwise”初始化操作会导致“bitwise copy”。这样效率高,也安全。
    实现copy constructor的最简单方法像这样:
    1
    2
    3
    4
    5
    6
    Point3d::Point3d(const Point3d &rhs)
    {
    _x = rhs._x;
    _y = rhs._y;
    _z = rhs._z;
    };
    但使用C++library的memcpy()会更有效率:
    1
    2
    3
    4
    Point3d::Point3d(const Point3d &rhs)
    {
    memcpy(this, &rhs, sizeof(Point3d));
    };

2.4 成员们的初始化队伍(Member Initialization List)

  • 当你写下一个constructor时,就有机会设定class members的初值,不是在member initialization list,就是在constructor函数本体。
  • 下列情况下,为了让你程序能够顺利执行,必须使用member initialization list:
    1. 当初始化reference member时
    2. 当初始化const member时
    3. 当调用一个base class的constructor,且拥有一组参数时
    4. 当调用一个member class的constructor,而 它拥有一组参数时
      这四种情况都能正确编译并执行,但效率不高。例如:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      class Word {
      String _name;
      int _cnt;
      public:
      Word() {
      _name = 0;
      _cnt = 0;
      }
      };
      constructor可能的内部扩张结果:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      Word::Word( /* this pointer goes here */ )
      {
      // String default constructor
      _name.String::String();
      // tempory
      String temp = String(0);
      // memberwise copy _name
      _name.String::operator=(temp);
      // destroy tempory
      temp.String::~String();
      _cnt = 0;
      }
      一个有效率的实现方法是:
      1
      2
      3
      4
      Word::Word : _name(0)
      {
      _cnt = 0;
      }
      它会扩张:
      1
      2
      3
      4
      5
      6
      Word::Word( /* this pointer goes here */ )
      {
      // String(int) constructor
      _name.String::String(0);
      _cnt = 0;
      }