爱技术 & 爱分享
爱蛋蛋 & 爱生活

Copy Constructor(复制构造函数)的构造操作

好吧看标题,应该知道本篇文章想瞎扯点什么了吧…
没错,你很聪明,这篇文章就带大家详细了解一下Copy Constructor 的构造操作…

来来来 开篇第一问:那些情况下一个 object 会以另外一个 class object 的内容作为初值?

不卖关子,情况有三:

情况一:显式的初始化操作
class X{···};
X x;
//显式的以 x 作为 xx 的初始值
X xx=x;
情况二:object被当做参数传递给函数时
extern void foo(X x);
void bar()
{
    X xx;
    // 以xx 作为参数(隐式初始化操作)
    foo(xx);
}
情况三:当函数返回一个object时
X foo_bar()
{
    X xx;
    //···
    return xx;
}

假如 class 设计者 显式的定义了一个copy constructor(第一个参数类型是起 class type)像下面一样:

//user-defined copy constructor 的实例
//参数可以有多个,但是从第二个及以后的参数必须提供默认值
X::X(const X& x);
Y::Y(const Y& y,int count =0 );

那么在大部分情况下,当一个 class object 以同类型的另一个实例作为初值,上述的代码会被调用。这可能导致一个临时性的 class object 产生或导致程序代码蜕变(也可能两者皆有)

Default Memberwise Initialization

如果 class 没有提供一个 explicit(显式的) constructor 会发生什么呢?

当 class object 以 “相同 class 的另一个 object”作为初值,其内部是以 default memberwise(按成员,逐成员) initialization 手法完成的,也就是把每一个内建的或派生的 data member(例如一个指针或一个数组)的值,从某个 object 拷贝一份到另一个 object 身上。不过它并不会拷贝其中的 member class object,而是以递归的方式施行memberwise initialization

class String
{
public:
//....没有 explicit copy constructor
private:
    char *str;
    int len;
}

一个 String object 的 default memberwise initialization 发生在这种情况下:

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

其完成方式,就类似以下的手法

//语意相等
verb.str=noun.str;
verb.len=noun.len;

如果一个 String object 被声明为另一个 class 的 member,像这样:

class  Word
{
public:
    //....没有 explicit copy constructor
private:
    int occurs;
    String word;    //此处 String object 成为 class Word 的一个 member
}

那么一个 Word 的 default memberwise initialization 会拷贝其 内建的 member occurs ,然后再于 String member object word 身上实施递归操作 memberwise initialization

一个良好的编译器可以为大部分 class object 产生 bitwise(按字节的,逐字节的) copies, 只要它们有 bitwise copy semantic(语意)……

注意

也就是说 “如果一个 class 未定义出 copy constructor, 编译器就自动为它产生出一个”,这句话是不对的。

Copy Constructor 应该跟 Default Constructor 一样,在必要的时候由编译器产生。

那么接下来重点就是这个”必要的时候”是什么时候? 其实是指 class 不展现出 bitwise copy semantic 的时候。

就像 default constructor 一样,C++ standard 上说,如果 class 没有声明一个 copy constructor , 就会有隐式的声明(implicit declare)或隐式的定义(implicit define)出现。

和之前文章里讲过的一样,C++ standard 把 copy constructor 区分为 trivial 和 nontrivial 两种。 只有 nontrivial 的实力才会被合成到程序中。

决定哪一个 copy constructor 是否为 trivial 的标准在于 class 是否展现出所谓的 "bitwise copy constructor"

Bitwise Copy Semantics

关于这个话题,我们换个方式来讨论,我们讨论什么时候不展先出Bitwise Copy Semantics

  • 当 class 内含一个 member object 而后者的 class 声明有一个 copy constructor 时(不论是被设计者显式的声明,亦或者被编译器合成)

  • 当 class 继承自一个 base class 而后者存在一个 copy constructor 时(再次强调,无论是是被显式声明亦或是被编译器合成)

  • 当 class 声明了一个或多个 virtual function时

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

前强两种情况,编译器必须将 member 或 base class 的 “copy constructors 调用操作” 安插到被合成的 copy constructor 中。

至于后面两种情况 由于比较复杂接下来详细讨论:

正确的初始化 vptr

首先我们来回忆一下当一个 class 声明了一个或多个 virtual function 时 constructor 会存在的扩张操作。

  • 增加一个 virtual function table(vtbl),内含每一个一个有效的 virtual function 的地址
  • 在每一个 class object 中安插一个指向 virtual function table 的指针(vptr)

很明显,对于每一个新产生的 class object 如果 vptr 不能成功的初始化,那么将会发生很严重的后果。

因此,当编译器导入一个 vptr 到 class中时,该 class 就不再符合 bitwise semantics 了(至于具体原因,别担心,后面会有)。

所以,编译器必须合成出一个 copy constructor 以求将 vptr 正确的初始化。

看下面这个例子:

class ZooAnimal
{
public:
    ZooAnimal();
    virtual ~ZooAnimal();
    virtual void Animate();
    virtual void Draw();
    //...
private:
    //...
};

class Bear:public ZooAnimal
{
public:
    Bear();
    virtual~Bear();
    virtual void Animate();
    virtual void Draw();
    virtual void Dance();
    //...
private:
    //...
};

ZooAnimal class object 以另一个 ZooAnimal class object 作为初值,或 Bear class object 以另外一个 Bear class object 作为初值,都可以直接靠”bitwise copy semantics”完成(当然有指针成员并且需要深复制的时候除外,此处不讨论)

Bear bearA;
Bear bearB=bearA;

bearB 会被 bearA 初始化。而在 constructor 中,bearB的 vptr 会被设定指向 Bear class 的 virtual table (靠编译器安插代码完成)。

所以呢,把 bearA 的 vptr 值拷贝给 bearB 是安全的。

bearA 和 bearB 的关系如下:
Relationship

当一个 base class object 以其 drived class 的 object 内容作为初始化操作时,其 vptr 复制操作也必须保证安全。例如:

ZooAnimal zooAnimal=bearA;  //这会发生切割

zooAnimal 的 vptr 肯定是不能指向 Bear class 的 virtual table 的(但是如果实施 bitwise copy的话 就会造成这种情况),否则下面的代码就真的爆炸了

void Draw(const ZooAnimal & something)
{
    something.Draw();
}
void Foo()
{
    //zooAnimal 的 vptr 指向 ZooAnimal 的virtual table  而不是 Bear 的 virtual table 否则的话 下面的代码跑出来的结果肯定不是你想要的
    ZooAnimal zooAnimal=bearA;
    Draw(zooAnimal);    //调用ZooAnimal::Draw()
    Draw(bearA);    //调用Bear::Draw()
}

zooAnimal 与 bearA 的真实关系如下:

Relationship

也就是说 合成出来的 ZooAnimal copy constructor 会显式的设定其 object的vptr 指向 ZooAnimal class 的 virtual table,而不是直接从右边的class object 将其 vptr 现值拷贝过来。

处理Virtual Base Class Subobject

Virtual base class 的存在也需要特别处理。一个class object 如果以另一个 object为初值,而后者有一个 virtual base class object 那么也会使 “bitwise copy semantics” 失效。

每一个编译器对于虚继承的支持承诺,都代表必须让 “derived clas object 中的 virtual base class subobject 位置” 在执行期准备妥当。

维护”位置完整性是编译器的责任”。”Bitwise copy semantic” 可能会破坏这个位置,所以编译器必须在它自己合成出来的 copy constructor 中做出仲裁。

例如下面的代码:

class  Raccoon : virtual public ZooAnimal
{
public:
     Raccoon(); //设定其自身的成员的初值
    ~ Raccoon();
    //...
private:
    //...
};
class Panda : public Raccoon
{
public:
    Panda();    //设定其自身的成员的初值
    ~Panda();
    //...
private:
    //...
};

继承结构如下:

Inherit

在 class Raccoon 中,编译器所产生的代码(用以调用ZooAnimal的 default constructor,将 Raccoon 的 vptr 初始化,并定位出 Raccoon 中的 Zooanimal subobject)将被安插在 Raccoon constructor 之内,并于所有用户代码之前执行。

注意

一个 virtual base class 的存在会使 copy semantics 失效。但是这个失效并不发生在 “一个 class object 以另一个 同类的 object 作为初值”时。而是发生在 “一个class object 以其 derived class 的某个 object 作为初值”时。

所以下面的操作是符合 bitwise copy 的

Raccoon raccoonA;
Raccoon raccoonB=raccoonA;//符合 bitwise copy

Panda pandaA
Panda pandaB=pandaA;//符合 bitwise copy

但是如果是以 pandaA 初始化 raccoonC时。编译器就必须判断后续当程序员企图存取其 ZooAnimal subobject 时,能否正确执行。

//简单的 bitwise copy 还不够
// 变异去必须显式地将 raccoonC 的 virtual base class pointer/offset 初始化)
Raccoon raccoonC=pandaA

关于 virtual base class pointer/offset的 描述请看我之前的文章 C++对象模型演变

这种情况下,为了完成正确的 raccoonC 的初值设定,编译器必须合成一个 copy constructor ,安插一些代码完成 virtual base class pointer/offset 的初值设定(或者是简单的确定它没有被抹掉)工作,对于每一个 member 执行必要的 memberwise 初始化操作, 以及执行其他的内存相关工作。

Sliced

但是下面这个问题就有趣多了,编译器无法知道”bitwise copy semantics”是否还保持着,因为它无法知道Raccoon 指针是否指向一个真正的 Raccoon object 或指向一个 derived class object:

//bitwise copy 正确与否,两说
Raccoon *ptr;
Raccoon raccoonD = *ptr;

那么当一个初始化操作存在而且还符合 “bitwise copy semantics” 的状态时,如果编译器能够保证 object 有正确而相等的初始化操作,它是否该压抑 copy constructor 的调用,以使其代码产生优化?

至少在 copy constructor 是编译器合成的时候,程序的副作用可能性为零。所以优化似乎是合理的。

如果 copy constructor 是由 class 设计者提供的呢? 烦请继续耐心阅读。

总结:

我们已经看过了四种情况下 class 不再保持 “bitwise copy semantics” 而且default copy constructor 如果未被声明的话,会被视为 nontrivial。

在这四种情况下,如果缺乏一个已声明的 copy constructor,编译器为了 正确处理 “以一个 class object 作为另一个 class object 的初值”,必须合成一个 copy constructor 。

Copy Constructor 要还是不要?

那么我们再来谈一下,需要手动指定一个Copy Constructor吗?通过前面的介绍相信大家心中都已经有数了。

通常情况下 bitwise copy 已经够用,且相当高效的时候,我们没必要再手动的指定一个 copy constructor。但是这里也说一种例外的情况,如果当你编译器NRV的触发,依赖于 copy constructor的时候,就需要我们手动提供一个(不过现代编译器在这方面都相当的智能,一般不存在这个问题,Lippman这里写的是很早之前的实现方式了)。

早期的 cfront需要一个开关来决定是否应该对代码实行NRV优化,这就是是否有程序员显式提供的拷贝构造函数:如果程序员没有显示提供拷贝构造函数,那么cfront认为程序员对默认的逐位拷贝语义很满意,由于逐位拷贝本身就是很高效的,没必要再对其实施NRV优化;但如果程序员显式提供了拷贝构造函数,这说明程序员由于某些原因(例如需要深拷贝等)摆脱了高效的逐位拷贝语义,其拷贝动作开销将增大,所以将应对其实施NRV 优化,其结果就是去掉并不必要的拷贝函数调用。

参考自:理解NRV优化

关于NRV的介绍请看此处C++程序转化(NRV)

赞(0) 传送门
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。编程日志 » Copy Constructor(复制构造函数)的构造操作
分享到: 更多 (0)

游戏 && 后端

传送门传送门