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的策略,以及這些策略如何影響程序。