上一篇文章介绍了编译器为default constructor在代码背后所做的处理,本文则讲解copy constructor背后的故事。

copy constructor

有三种情况,会用到copy constructor

class X
{
    ...
}

X xx = x;
extern void foo(X x);
void bar()
{
    X xx;
    foo(xx);
}
X foo_bar()
{
    X xx;
    ...
    return xx;
}

如果类的设计者显示的定义的copy constructor:

X::X(const X& x) { ... }

在上面的三种情况下,copy constructor都会被调用。然而,如果类没有提供显示的copy constructor,编译器会做怎样的处理呢?

default member-wise initialization

如果一个类的实例以另一个实例作为初值,如X xx = x;,且没有提供显示的copy constructor,后者会被以default member-wise的手法进行其内部所有成员的初始化:

class String
{
public:
    char* str;
    int len;
};

String noun("book");
String verb = noun;

default member-wise的初始化会对String中每一个成员初始化:

verb.str = noun.str;
verb.len = noun.len;

如果String作为成员存在某一个类中,对这个类的default member-wise initialization会是递归的。参考下面的代码:

class Word
{
public:
    // 没有显示的copy constructor
private:
    int m _occurs;
    String m_word;
}

Word的default member-wise initialization会复制成员m_occurs,并且对m_word中的成员递归实行赋值。我的理解是,default member-wise initialization是编译器实现的bit-wise的浅拷贝,String verb = noun;memcpy(&verb, &noun,sizeof(String));
ARM指出,default member-wise initialization是被copy constructor实现出来的。联想到default constructor,你或许会疑惑,如果一个类没有定义copy constructor,编译器是否一定为它合成出一个copy constructor,而不是像default constructor一样,只在编译器需要的时候才被合成出来?《对象模型》指出,答案是后者,copy constructor也只在必要的时候被合成出来。对于default constructor,有四种情况编译器不会把它合成出来;而对于copy constructor,必要的情况指的是当类不展现bit-wise copy语义的时候。
和default constructor一样,copy constructor也有trivial和non-trivial之分,只有当copy constructor是non-trivial的时候它才会被隐式地合成出来。决定它是否是trivial则取决于类是否展现bit-wise copy的语义。

bit-wise copy语义

接下来探究什么时候类会展现出bit-wise copy的语义。考虑下面的代码:

#include "Word.h"

Word noun("book");
void foo()
{
    Word verb = noun;
}

Word.h里面包含了Word的详细定义。但是在看到Word的内容之前,我们不知道Word的设计者是否提供了copy constructor,所以我们无法预测这个初始化的行为。如果Word没有显式定义copy constructor,编译器是否会为它产生一个呢?这得看Word是否展现bit-wise语义。看看下面Word的声明:

class Word
{
public:
    Word(const char*);
    ~Word() { delete[] str; }
private:
    int cnt;
    char* str;
};

《对象模型》一书中说,此时Word展现出了default copy的语义,所以不会有default copy constructor被合成出来。其实对于这一段我很疑惑,如果没有copy constructor被合成出来,是如何施行default member-wise initialization手法,为verb中的每个成员赋值的呢?书中提到这种情况下不会产生函数调用,是不是意味着Word verb = noun会被展开成verb.str = noun.str; verb.len = noun.len;?
书中也给出了Word的另一种声明,即Word不展现bit-wise copy语义的情况:

class Word
{
public:
    Word(const String&);
    ~Word();
private:
    int m_cnt;
    String m_str;
};

class String
{
public:
    String(const char*);
    String(const String&);
    ~String();
};

Word中的String类显示声明了copy constructor,为此编译器需要合成出copy constructor,其中包含了调用String的copy constructor的代码:

inline Word::Word(const Word& wd)
{
    str.String::String(wd.str);
    m_cnt = wd.m_cnt;
}

当然,Word中可能存在的,注入整数、指针等成员也会被复制。

四种不展现bit-wise copy语义的情况

  1. 当类中包含了一个对象,且后者拥有一个copy constructor。这个copy constructor可以是显式声明的,如前面的String,也可以是被合成出来的,如Word
  2. 当类继承自某个基类,且后者拥有一个copy constructor。again,copy constructor可以是显示的或隐式的。
  3. 当类中声明了虚函数。
  4. 当类拥有虚基类。

前两点和default constructor类似,已有文章介绍。下面讨论情况3和4。

类中有虚函数

当类中声明了虚函数,编译器会对其成员进行扩张,即:

  • 增加一个虚函数表vtbl;,包含每个虚函数的地址。
  • 增加一个虚表指针vptr指向vtbl。

显然需要对对象中的vptr进行精心设置保证正确的行为,这个类也因此不再有bit-wise copy的语义了。编译器需要合成出一个copy constructor来设置vptr。

class ZooAnimal
{
public:
    ZooAnimal();
    virtual ~ZooAnimal();
    virtual void animate();
    virtual void draw();
private:
    // some data
};
class Bear: public ZooAnimal
{
public:
    Bear();
    void animate();
    void draw();
    virtual void dance();
private:
    // some data
};
ZooAnimal alpha;
ZooAnimal beta = alpha;
Bear gamma;
Bear delta = gamma;

最后的两种赋值都可以通过bit-wise语义完成,alpha的vptr被拷贝给beta的vptr,gamma的vptr被拷贝给delta的vptr,因为vtbl是一样的,所以是安全的。而如果基类对象从其子类对象赋值而来:

ZooAnimal epsilon = gamma;

epsilon的vptr不可以被设定为指向基类Bear的vtbl,bit-wise语义因此失效。如果强行对epsilon进行bit-wise copy的话,多态会受到影响。因此需要在ZooAnimal的copy constructor中显式设定vptr指向ZooAnimal的vtbl,而不是从右值中获得。

void Draw(const ZooAnimal& zoey) { zoey.draw(); }
void foo()
{
    ZooAnimal epsilon = gamma;
    Draw(epsilon);
    Draw(gamma);
}
类拥有虚基类

bit-wise copy也会对虚继承造成破坏。考虑下面的例子:

class Raccoon: public virtual ZooAnimal
{
public:
    Raccoon() { /* 设定private data初值*/ }
    Raccoon(int val) { /* 设定private data初值*/ }
private:
    // some data
};

class RedPanda: public Raccoon
{
public:
    RedPanda() { /* 设定private data初值*/ }
    RedPanda(int val) { /* 设定private data初值*/ }
private:
    // some data
};

RedPanda panda;
Raccoon raccoon = panda;

问题出在RedPanda中的Raccoon成分,编译器需要在copy constructor中将raccoon的虚基类指针和偏移量准备妥当。