C++对象模型——Copy Constructor 的建构操作(第二章)

来源:互联网 发布:心算除法的快速计算法 编辑:程序博客网 时间:2024/05/29 04:18

2.2    Copy Constructor 的建构操作

        有三种情况,会以一个object的内容作为另一个 class object的初值,最明显的一种情况就是对一个object做显式的初始化操作,例如:
class X { ... };X x;// 明确地以一个object的内容作为另一个class object的初值X xx = x;
    另两种情况是当object被当作参数交给某个函数时,例如
extern void foo(X x);void bar() {    X xx;    // 以xx作为foo()第一个参数的初值(隐式的初始化操作)    foo(xx);}
    以及当函数传回一个 class object时。例如:
X foo_bar() {    X xx;    return xx;}
    假设 class 设计者明确定义了一个copy constructor(这是一个constructor,有一个参数的类型是其 class type),例如:
// user-defined copy constructor的实例// 可以是多参数形式,其第二个参数及后继参数以一个默认值供应之X::X(const X &x);Y::Y(const Y &y, int = 0);
    那么在大部分情况下,当一个 class object以另一个同类实体作为初值时,上述的constructor会被调用。这可能会导致一个暂时性 class object的产生或程序代码的蜕变。

Default Memberwise Initialization

    如果 class 没有提供一个explicit copy constructor会怎样?当 class object以"相同class的另一个object"作为初值时其内部是以所谓的default memberwise initialization完成的,也就是把每一个内建的或派生的data member(例如一个指针或数组)的值,从某个object拷贝一份到另一个object,不过它并不会拷贝其中的member class object,而是以递归的方式施行memberwise initialization。例如,考虑下面这个 class 声明:
class String {public:    // ... 没有 explicit copy constructorprivate:    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,如下所示:
class Word {public:    // ... 没有 explicit copy constructorprivate:    int _occurs;    String _word;    // String object成为class Word的一个member};
    那么一个Word object的default memberwise initialization会拷贝其内建的member _occurs,然后再从String member object _word递归实施memberwise initialization。
    这样的操作如何实际上是怎样完成的?ARM指出:
    从概念上而言,对于一个 class X,这个操作是被一个copy constructor实现出来。
    关键的是"概念上",这个注释紧跟着一些解释:
    一个良好的编译器可以为大部分 class objects产生bitwise copies,因为它们有bitwise copy semantics...
    也就是说,"如果一个class未定义出copy constructor,编译器就自动为它产生出一个"这句话是不对的,而是应该像ARM所说:
    Default constructors和copy constructors在必要的时候采油编译器产生出来。
    这个句子的"必要"是指 class 不展现bitwise copy semantics时。C++ Standard仍然保留了ARM的意义,但将相关讨论更形式化如下:
    一个 class object可以从两种方式复制得到,一种是被初始化,另一种是被指定(assignment)。从概念上而言,这两个操作分别是以copy constructor和copy assignment operator 完成的。
    就像default constructor一样,C++ Standard指出,如果 class 没有声明一个copy constructor,就会有隐式的声明出现。C++ Standard把copy constructor区分为trivial和nontrivial两种,只有nontrivial的实体才会被合成于程序中,决定一个copy constructor是否为trivial的标准在于 class 是否展现出所谓的"bitwise copy semantics"。

Bitwise Copy Semantics (位逐次拷贝)

    在下面的程序片段中:
#include "Word.h"Word noun("book");void foo() {    Word verb = noun;}
    很明显verb是根据noun来初始化,但在尚未看到 class Word声明之前,不可能预测这个初始化操作的程序行为,如果 class Word的设计者定义了一个copy constructor,verb的初始化操作会调用它,但如果该 class 没有定义explicit copy constructor,那么是否会有一个编译器合成的实体被调用呢?这就视该 class 是否展现"bitwise copy semantics"而定。如下所示:
// 以下声明展现了bit copy semanticsclass Word {public:    Word(const char *);    ~Word() {        delete []str;    }private:    int cnt;    char *str;};
    这种情况下并不需要合成出一个default copy constructor,因为上述声明展现了"default copy semantics",因此verb的初始化操作就不需要一个函数调用,然而,如果 class Word是这样声明的:
// 以下声明并未展现出bitwise copy semanticsclass 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();};
    在这种情况下,编译器必须合成出一个copy constructor以便调用member class String object的copy constructor:
// 一个被合成出来的copy constructor// C++伪代码inline Word::Word(const Word &wd) {    str.String::String(wd.str);    cnt = wd.cnt;}
    有一点值得注意:在这被合成出来的copy constructor中,如整数、指针、数组等等的nonclass members也都会被复制。

不要Bitwise copy Semantics

    什么时候一个 class 不展现出"bitwise copy semantics"呢?有四种情况:
    1.    当 class 内含一个member object而后者的 class 声明有一个copy constructor时(不论是被 class 设计者显式声明,或是被编译器合成)
    2.    当 class 继承自一个base class 而后者存在有一个copy constructor时
    3.    当 class 声明了一个或者多个 virtual functions时
    4.    当 class 派生自一个继承串链,其中有一个或者多个 virtual base classe

    前两种情况中,编译器必须将members或base class 的"copy constructors调用操作"插入到被合成的copy constructor中。后两种情况有点复杂,如接下来的小节所述。

重新设定 Virtual Table的指针

    编译期间的两个程序扩张操作(只要有一个 class 声明了一个或多个 virtual functions就会如此)
    增加一个 virtual function table(vtbl),内含每一个有作用的 virtual function的地址
    将一个指向 virtual funtcion table的指针(vptr),插入到每一个 class object中

    很显然,如果编译器对于每一个新产生的 class object的vptr不能成功而正确地设好其初值,将导致可怕的后果。因此,当编译器导入一个vptr到 class 中时,该 class 就不再展现bitwise semantics。现在,编译器需要合成出一个copy constructor,以求vptr适当初始化,如下所示:
    首先,定义两个classes,ZooAnimal和Bear
class ZooAnimal {public:    ZooAnimal();    virtual ~ZooAnimal();    virtual void animate();    virtual void draw();private:    // ZooAnimal的animate()和draw()    // 所需要的数据};class Bear : public ZooAnimal {public:    Bear();    void animate();    void draw();    virtual void dance();private:    // Bear的animate()和draw()和dance()    // 所需要的数据};
    ZooAnimal class object以另一个ZooAnimal class object作为初值,或Bear class object以另一个Bear class object作为初值,都可以直接靠"bitwise copy semantics"完成。例如:
Bear yogi;Bear winnie = yogi;
    yogi会被 default Bear constructor初始化,而在constructor中,yogi的vtpr被设定指向Bear class 的 virtual table。因此,把yogi的vptr值拷贝给winnie的vptr是完全的。
    当一个base class object以其derived class 内容做初始化操作时,其vptr复制操作也必须保证安全,例如:
ZooAnimal franny = yogi;    // 这会发生切割(sliced)
    franny的vptr不可以被设定指向Bear class 的virtual table,否则当下面程序片段中的draw()被调用而franny被传进去时,就会"炸毁"(blow up)
void draw (const ZooAnimal &zoey) {    zoey.draw();}void foo() {    // franny的vptr指向ZooAnimal的virtual table    // 而非Bear的virtual table    ZooAniaml franny = yogi;    draw(yogi);        //调用Bear::draw()    draw(franny);    //调用ZooAnimal::draw()}
    通过franny调用virtual function draw(),调用的是ZooAnimal实体而非Bear实体(虽然franny是以Bear object yogi作为初始值)。因为franny是一个ZooAnimal object。事实上,yogi中的Bear部分已经在franny初始化时被切割(sliced)。如果franny被声明为一个reference(或者如果它是一个指针,而其值为yogi的地址),那么经由franny所调用的draw()才会是Bear的函数实体。
    合成出来的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 subobject,那么也会使"bitwise copy semantics"失效。
    每一个编辑器对于虚拟继承的支持承诺,都表示必须让"derived class object中的virtual base class subobject位置"在执行期就准备妥当。维护"位置的完整性"是编辑器的责任。"Bitwise copy semantics"可能会破坏这个位置,所以编辑器必须在它自己合成出来的 copy constructor中做出仲裁。例如,在下面的声明中,ZooAnimal成为Raccon的一个virtual base class :
class Raccon : public virtual ZooAnimal {public:    Raccon(){ /* 设定private data初值 */ }    Racccon(int val) { /* 设定private data初值 */ }    // ...private:    // 所需要的数据};
    编译器所产生的代码(用以调用ZooAnimal的default constructor,将Racccon的vptr初始化,并定位出Raccon中的ZooAnimal subject)被插入在两个Raccon constructors之间。
    那么"memberwise初始化"呢?一个 virtual base class 的存在会使bitwise copy semantics无效。其次,问题并不发生于"一个class object以另一个同类object作为初值",而是发生于"一个class object以其derived classes的某个object作为初值".例如让Racccon object以一个RedPanda object作为初值,而RedPanda声明如下:
class RedPanda : public Raccon {public:    RedPanda() { /* 设定private data初值 */ }    RedPanda(int val) { /*设定private data初值 */ }private:    // ...};
    如果以一个Reccon object作为另一个Raccon object的初值,那么"bitwise copy"就戳戳有余了
// 简单的bitwise copy就足够Raccon rocky;Raccon little_critter = rocky;
    然而如果企图以一个RedPanda object作为little_critter的初值,编译器必须判断"后续当程序员企图存取其ZooAnimal subobject时是否能够正确地执行"
// 简单的bitwise copy还不够// 编译器必须明确地将litte_critter的virtual base class pointer/offset初始化RedPanda little_red;Raccon little_critter = little_red;
    在这种情况下,为了完成正确的little_critter初值设定,编译器必须合成一个copy constructor,插入一些码以设定 virtual base class pointer/offset的初值,对每一个members执行必要的memberwise初值化操作,以及执行其它的内存相关操作(3.4对于 virtual base classes有更详细的讨论)
    在下面的情况中,编译器无法知道是否"bitwise copy semantics"还保持着,因为它无法知道Raccon指针是否指向一个真正的Raccon object,还是指向一个derived class object:
// 简单的bitwise copy可能够用,可能不够用Raccon *ptr;Raccon little_critter = *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。下一节介绍编译器调用ocpy constructor的策略,以及这些策略如何影响程序。

0 0