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

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 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,如下所示:

class Word {
public:
    // ... 没有 explicit copy constructor
private:
    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 semantics
class Word {
public:
    Word(const char *);
    ~Word() {
        delete []str;
    }
private:
    int cnt;
    char *str;
};

这种情况下并不需要合成出一个default copy constructor,因为上述声明展现了"default copy semantics",因此verb的初始化操作就不需要一个函数调用,然而,如果 class Word是这样声明的:

// 以下声明并未展现出bitwise copy semantics
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();
};

在这种情况下,编译器必须合成出一个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的策略,以及这些策略如何影响程序。

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-09 21:35:11

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

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

第2章    构造函数语意学 (The Semantics of Constructor) 关于C++,最常听到的一个抱怨就是,编译器背着程序员做了太多事情.Conversion运算符就是最常被引用的一个例子. 2.1    Default Constructor的建构操作 C++ Annotated Reference Manual(ARM)指出"default constructors ...在需要的时候被编译器产生出来".关键字眼是"在需要的时候".被谁需要?

构造函数语义学——Copy Constructor的建构操作

?在三种情况下,会以一个object的内容作为另一个class object的初值: object明确初始化 123 class {...};X x;X xx = x; object被当作参数交与某个函数 12345 extern void foo(X x);void bar(){ X xx; foo(xx);} 函数返回值是一个class object 12345 X foo_bar(){ X xx; ... return xx;} 如果开发者已经明确定义了一个copy constructor

深入探索c++对象模型->2.2 Copy Constructor的构造操作

一.只有当class不展现出bitwise copy semanties时,编译器才会为class生成copy constructor.那么当什么时候回出现非bitwise copy呢? 1.当class内含一个member object而后者的class声明有一个copy constructor时(不论是显示声明或是被合成得到的): 2.当class继承自一个base class而后者存在一个copy constructor时. 3.当class声明了一个或多个virtual function

2.1 Default Constructor的建构操作

1.   存在错误的程序例子 class Foo { public:  int m_nVal;  Foo *m_pNext; }; void Foo_Bar( void ) {  Foo bar;    if ( bar.m_nVal || bar.m_pNext )  {   int i;   i++;  } } (1)正确的程序语意是要求 Foo 有一个 default constructor,可以将 members 初始化为0. (2)上述代码不会合成一个 default construc

C++对象模型——程序转化语意学(第二章)

2.3    程序转化语意学 (Program Transformation Semantics) 如下程序片段所示: #include "X.h" X foo() { X xx; return xx; } 看到这个代码,可能做出以下假设: 1.    每次foo()被调用,就传回xx的值 2.    如果 class X定义了一个copy constructor,那么当foo()被调用时,保证该copy constructor也会被调用. 第一个假设的真实性,必须视 class X如

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

在使用C++时,常常会好奇或者抱怨,编译器为我们做了什么事呢? 为什么构造函数没有为我初始化呢?为什么我还要写默认构造函数呢? 2.1 Default Constructor 的构造操作 如果没有声明默认构造函数,编译器会在需要的时候帮我们产生出来. 为了避免在多个地方被需要导致重复,则编译器将产生的构造函数声明为inline方式. class Foo {public:Foo(), Foo(int) }; class Bar {public: Foo foo;char *str;} Bar ba

深入探索C++对象模型->2.1 Default Constructor的构造操作

一.对于class X,有四种情况,会造成“编译器必须为未声明constructor的classes合成一个default constructor或者扩充user-constructor”,对于其它情况,编译器什么也不做,如果程序需要,那是程序员的责任. 1.带有Default Constructor的Member Class Object:“如果class A内含一个或一个以上的member class objects,那么编译器会为没有构造函数的类添加构造函数来调用每一个member cla

【深度探索C++对象模型】第二章 构造函数语意学(上)

第二章 构造函数语意学(The Semantics of Constructors) -- 本书作者:Stanley B.Lippman 一.前言 首先让我们来梳理一个概念: 默认构造函数(Default Constructor) : 是在没有显示提供初始化式时调用的构造函数.它由不带任何参数的构造函数,或是为所有形参提供默认实参的构造函数定义.如果定义的某个类的成员变量没有提供显示的初始化式时,就会调用默认构造函数(Default Contructor). 如果用户的类里面,没有显示的定义任何

Copy constructor拷贝构造函数

翻译的是wikipedia关于copy constructor,地址:点击打开链接 拷贝构造函数是C++语言中用一个已有对象创建一个新对象的特殊构造函数,该函数的第一个参数必须是函数所在类型的引用(译注:const/non-const都可以,可以有多个参数剩余参数必须有默认值,一定要是引用,这些原因见后,问:有多个参数拷贝构造如何调用?). 通常编译器会自动为每一个class创建一个拷贝构造函数(显示拷贝构造);有些情况下程序员自定义了拷贝构造函数(用户自定义拷贝构造),这时编译器不合成拷贝构造