0%

深度探索C++对象模型之构造函数语意学

2章:构造函数语意学

在这一章中,主要介绍编译器对于“对象构造过程”的干涉,以及对于“程序形式”和“程序效率”的冲击。

2.1 Default Constructor的构造操作

对于未声明构造函数的类,只有在以下四种情况下编译器才会为它们合成默认构造函数:

  • 类有一个类对象(Member Class Object)成员,且该成员含有默认构造函数(Default Constructor
  • 继承自带有默认构造函数(Default Constructor)的基类(Base class
  • 带有虚函数(Virtual function)的类
  • 继承自虚基类(Virtual base class)的类

对于以上四种情况,C++标准把合成的默认构造函数叫隐式的有意义默认构造函数(implicit nontrivial default constructors)。被合成的构造函数只能满足编译器(而非程序)的需要,它之所以能够完成任务,是借着调用成员对象或基类的默认构造函数(情况1/2),或是为每一个对象初始化其虚函数机制或虚基类机制(情况3/4)。

至于没有存在上述四种情况,而又没有声明任何构造函数的类,那么它们拥有的是隐式无意义默认构造函数(implicit trivial default constructors),实际上它们并不会被合成出来。

Default Constructor会在编译器需要的时候被产生出来,被合成出来的Constructor只执行编译器所需要的行为。如果程序有需要,定制Default Constructor的行为是程序员的责任。

  1. “带有Default Constructor”的Member Class Object

    如果有多个class member objects都要求constructor初始化操作,将如何呢?C++语言要求以“member objectsclass中的声明次序”来调用各个constructors。这一点由编译器完成,它为每一个constructor安插程序代码,以“member声明次序”调用每一个member所关联的default constructors。这些码将被安插在explicit user code之前。举个例子:

    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
    32
    33
    class Dopey
    {
    public:
    Dopey();
    ...
    };

    class Sneezy
    {
    public:
    Sneezy(int);
    Sneezy();
    ...
    };

    class Bashful
    {
    public:
    Bashful();
    ...
    };


    class Snow_White
    {
    ...
    private:
    Dopey dopey;
    Sneezy sneezy;
    Bashful bashful;

    int mumble;
    };

    如果Snow_White没有定义default constructor,就会有一个nontrivial constructor被合成出来,依序调用DopeySneezyBashfuldefault constructors。然而如果Snow_White定义了下面这样的default constructor

    1
    2
    3
    4
    5
    // 程序员所写的 default constructor
    Snow_White::Snow_White() : sneezy(1024)
    {
    mumble = 2048;
    }

    它会被扩张为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 编译器扩张后的 C++ 伪码
    Snow_White::Snow_White()
    {
    // 调用其 constructor
    dopey.Dopey::Dopey();
    Sneezy.Sneezy::Sneezy(1024);
    bashful.Bashful::Bashful();

    // explicit user code
    mumble = 2048;
    }
  2. “带有Default Constructor”的Base Class

    类似的道理,如果一个没有任何constructorsclass派生自一个“带有default constructor”的base class,那么这个derived classdefault constructor会被视为nontrivial,并因此需要被合成出来。它将调用上一层base classesdefault constructor(根据它们的声明次序)。对一个后继派生的class而言,这个合成的constructor和一个“被明确提供的default constructor”没有什么差异。

    如果设计者提供多个constructors,但其中都没有default constructor呢?编译器会扩张现有的每一个constructors,将“用以调用所有必要之default constructors”的程序代码加进去。它不会合成一个新的default constructor,这是因为其它“由user所提供的constructors”存在的缘故。如果同时亦存在着“带有default constructors”的member class objects,那些default constructor也会被调用。

  3. “带有(声明或继承)一个Virtual Function”的Class

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Widget
    {
    public:
    virtual void flip() = 0;
    ...
    };

    void flip(const Widget& widget)
    {
    widget.flip();
    }

    // 假设 Bell 和 Whistle 都派生自 Widget
    void foo()
    {
    Bell b;
    Whistle w;

    flip(b);
    flip(w);
    }

    下面两个扩张操作会在编译期间发生:

    1. 一个virtual function table会被编译器产生出来,内放classvirtual functions地址。

    2. 在每一个class object中,一个额外的pointer member会被编译器合成出来,内含相关的class vtbl的地址。

      此外,widget.flip()的虚拟引发操作会被重新改写,以使用widgetvptrvtbl中的flip()条目:

      1
      2
      3
      // 1 表示 flip() 在 virtual table 的固定索引
      // &widget 代表要交给"被调用的某个 flip() 实体"的 this 指针
      (*widget.vptr[1])(&widget));

      为了让这个机制发挥功效,编译器必须为每一个Widget(或其派生类之)objectvptr设定初值,放置适当的virtual table地址。对于class所定义的每一个constructor,编译器会安插一些码来做这样的事情。对于那些未声明任何constructorsclasses,编译器会为它们合成一个default constructor,以便正确地初始化每一个class objectvptr

  4. “带有一个Virtual Base Class”的Class

    编译器必须使virtual base class在其每一个derived class object中的位置,能够在执行期准备妥当。一种可能的做法是在derived class中安插一个指向每一个virtual base class的指针,所有“经由referencepointer来存取其中virtual base class中数据的操作”都通过此指针完成。

    同样地,这个virtual base class指针是在class object构建期间完成的,编译器的默认行为和3中的vptr的处理方式一样。

2.2 Copy Constructor的构造操作

有三种情况会调用copy constructor

  • 对一个class object做明确的初始化操作

    1
    2
    3
    4
    5
    class X {...};
    X x;

    X xx(x);
    X xx = x;
  • class object被当作参数交给某个函数时

    1
    2
    3
    4
    5
    6
    7
    extern void foo(X x);
    void bar()
    {
    X xx;
    foo(xx);
    ...
    }
  • 当函数返回一个class object

    1
    2
    3
    4
    5
    6
    X foo_bar()
    {
    X xx;
    ...
    return xx;
    }
  1. Default Memberwise Initialization

    如果class没有提供一个explicit copy constructor又当如何?当class object以“相同class的另一个object”作为初值时,其内部是以所谓的default memberwise initialization完成的,也就是把每一个内建的或派生的data member的值,从某个object拷贝一份到另一个object身上。不过它并不会拷贝其中的member class object,而是以递归的方式施行memberwise initialization

    一个良好的编译器可以为大部分class objects产生bitwise copies,因为它们有bitwise copy semanticscopy constructors必要的时候才由编译器产生出来。这个句子中的“必要”意指当class不展现bitwise copy semantics时。

  2. Bitwise Copy Semantics

    C++ Standardcopy constructor区分为trivialnontrivial两种。只有nontrivial的实体才会被合成于程序之中。决定一个copy constructor是否为trivial的标准在于class是否展现出所谓的“bitwise copy semantics”。

    什么时候一个class不展现出Bitwise Copy Semantics呢?

    • class内含一个member object而后者的class声明有一个copy constructor时(不论是被class设计者明确地声明;或是被编译器合成)。

    • class继承自一个base class而后者存在有一个copy constructor(再次强调,不论是被明确声明或是被合成而得)。

    • class声明了一个或多个virtual functions时。

    • class派生自一个继承串链,其中有一个或多个virtual base classes时。

      有一点很值得注意:在被合成出来的copy constructor中,如整数、指针、数组等等的nonclass members也都会被复制。

  3. 重新设定virtual table的指针vptr

    当编译器导入一个vptrclass之中时,该class就不再展现bitwise semantics了。编译器需要合成出一个copy constructor,以求将vptr适当地初始化。

    base class object以另一个base class object作为初值,或derived class object以另一个derived class object作为初值,都可以直接靠“bitwise copy semantics”完成。当一个base class object以其derived classobject内容做初始化操作时,其vptr复制操作必须保证安全。此时,合成出来的base copy constructor会明确设定objectvptr指向base classvirtual table,而不是直接从右手边的class object中将其vptr现值拷贝过来。

  4. 处理virtual base class subject

    3同理,需要合成copy constructor来明确的设定virtual base class pointer的初值。

2.3 程序转化语意学

已知有这样的定义:

1
X x0;

下面有三个定义,每一个都明显地以x0来初始化其class object

1
2
3
4
5
6
void foo_bar()
{
X xl(x0);
X x2 = x0;
X x3 = X(x0);
}

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

  1. 重写每一个定义,其中的初始化操作会被剥除。
  2. classcopy 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;

// 编译器安插X copy construction的调用操作
x1.X::X(x0);
x2.X::X(x0);
x3.X::X(x0);
}
  • 参数的初始化

    把一个class object当做参数传给一个函数,相当于以下形式的初始化操作:

    1
    2
    // xx 代表形式参数, arg 代表真正的参数值
    X xx = arg;
  • 返回值的初始化

    已知下面这个函数定义:

    1
    2
    3
    4
    5
    6
    X bar()
    {
    X xx;
    // 处理 xx ...
    return xx;
    }

    这里有一个双阶段转化:

    1. 首先加上一个额外参数,类型是class object的一个reference。这个参数将用来放置被“拷贝建构(copy constructed)”而得的返回值。

    2. return指令之前安插一个copy constructor调用操作,以便将欲传回之object的内容当做上述新增参数的初值。

      根据这样的算法,bar()转换如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      void bar(X &_result)
      {
      X xx;
      // 编译器所产生的 default constructor 调用操作
      xx.X::X();
      // ... 处理 xx
      // 编译器所产生的 copy constructor
      result.X::X(xx);
      return;
      }

      现在编译器必须转换每一个bar()调用操作,以反映其新定义。例如:

      1
      X xx = bar();

      将被转换为下列两个指令句:

      1
      2
      3
      // 注意, 不必施行 default constructor
      X xx;
      bar(xx);
  • NRV优化

    已知下面这个函数定义:

    1
    2
    3
    4
    5
    6
    X bar()
    {
    X xx;
    // 处理 xx ...
    return xx;
    }

    编译器会把它优化为:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    void bar(X &_result)
    {
    // default constructor 调用操作
    _result.X::X();

    // ... 直接处理 _result

    return;
    }

    只有当程序提供了explicit copy constructor,编译器才会实施NRV优化。没有提供的话(编译器自己合成的),是不会实施NRV优化的。

  • copy constructor要还是不要?

    对于下面的3D坐标点类,这个class的设计者应该提供一个explicit copy constructor吗?

    1
    2
    3
    4
    5
    6
    7
    8
    class Point3d
    {
    public:
    Point3d(float x, float y, float z);
    // ...
    private:
    float _x, _y, _z;
    };

    除非你预见该class需要大量的memberwise初始化操作,例如函数以传值(by value)的方式传回objects,那么提供一个copy constructorexplicit inline函数实体就非常合理——可以激活编译器提供的NRV优化。否则,不提供explicit copy constructor既快速又安全。

2.4 Member initialization list

为了让你的程序能够被顺利编译,必须使用member initialization list的情况:

  1. 当初始化一个refercnce member时;
  2. 当初始化一个const member时;
  3. 当调用一个base classconstructor,而它拥有一组参数时;
  4. 当调用一个member classconstructor,而它拥有一组参数时。

在这四种情况之外,不使用member initialization list,程序可以被正确编译并执行,但是效率不彰。例如:

1
2
3
4
5
6
7
8
9
10
11
12
class Word
{
public:
Word()
{
_name = 0;
_cnt = 0;
}
private:
String _name;
int _cnt;
};

下面是编译器对constructor可能的内部扩张结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// C++ 伪码
Word::Word(/* this pointer goes here */)
{
// 调用 String default constructor
_name.String::String();

// 产生临时性对象
String temp = String(0);

// memberwise 地拷贝 _name
_name.String::operator=(temp);

// 摧毁暂时性对象
temp.String::~String();

_cnt = 0;
}

更有效率的实现方法:

1
2
3
4
5
// 较佳的方式
Word::Word : _name(0)
{
cnt = 0;
}

它会被扩张成这个样子:

1
2
3
4
5
6
7
// C++ 伪码
Word::Word(/* this pointer goes here */)
{
// 调用 String(int) constructor
_name.String::String(0);
_cnt = 0;
}

编译器会一一操作initialization list,以member声明次序(不是由initialization list中的排列次序决定的)在constructor之内的explicit user code之前安插初始化操作。

调用一个member function以设定一个member的初值是可以的,因为此时this指针已经被构造妥当。

1
2
3
4
5
6
7
8
9
10
// X::xfoo() 被调用
X::X(int val) : i(xfoo(val)), j(val)
{ }

// 会被扩张为
X::X(/* this pointer */, int val)
{
i = this->xfoo(val);
i = val;
}

注意:如果一个derived class member function被调用,其返回值被当做base class constructor的一个参数,这不是一个好主意!!!