[讀書筆記] - 《深度探索C++對象模型》第2章 構造函數語意學

Table of Contents

 

1.Default Constructor的建構操作

1.2 nontrivial default constructor四種情況

1.2.1“帶有Default Constructor”的Member Class Object

1.2.2 “帶有Default Constructor”的Base Class

1.2.3 “帶有一個Virtual Function”的Class

1.2.4“帶有一個Virtual Base Class”的Class

2.Copy Constructor的建構操作

2.1 Bitwise Copy Semantics(位逐次拷貝)

2.2 不要Bitwise Copy Semantics

2.2.1 重新設定Virtual Table的指針

2.2.2 處理Virtual Base Class Subobject

3.成員們的初始化隊伍(Member Initialization List)

3.1 在initialization list中調用member function

3.2 調用member function初始化基類

4. Named Return Value(NRV)優化


1.Default Constructor的建構操作

1.1 C++ Annotated Reference Manual(ARM)中的Section12.1告訴我們:“default constructor...在需要的時候被編譯器產生出來”。

class Foo { public: int val; Foo *pnext; };

void foo_bar()
{
    // oops: 程序要求bar's members都被清爲0
    Foo bar;
    if(bar.val || bar.pnext)
        // ... do something
    // ...
}

在這個例子中,正確的程序語意是要求Foo有一個default constructor,可以將它的兩個members初始化爲0. 上面這段代碼可曾符合ARM所說的“在需要的時候”?答案是no。其間的差別在於一個是程序的需要,一個是編譯器的需要。程序如果有需要,那是程序員的責任;本例要承擔責任的是設計class Foo的人。是的,上述程序片段並不會合成出一個default constructor。

C++ Standard已經修改了ARM中的說法,雖然其行爲事實上仍然是相同的。

對於class X, 如果沒有任何user-declared constructor,那麼會有一個default constructor被隱式地(implicitly)聲明出來……

這個隱式地聲明出來的constructor可能是trivial(沒啥用的),也可能是nontrivial的。一個nontrivial default constructor在ARM的術語中就是編譯器所需要的那種,必要的話會由編譯器合成出來。

1.2 nontrivial default constructor四種情況

1.2.1“帶有Default Constructor”的Member Class Object

如果一個class沒有任何constructor,但它內含一個member object,而後者有default constructor,那麼這個class的implicit default constructor就是“nontrivial”,編譯器需要爲此class合成一個default constructor。不過這個合成操作只有在constructor真正需要被調用時纔會發生。

class Foo { public: Foo(), Foo(int) ... };
class Bar { public: Foo foo; char* str; };

void foo_bar()
{
    Bar bar; // Bar::foo必須在此處初始化。Bar::foo是一個member object,而其class Foo擁有default constructor
    if(str) { } ... 
}

編譯器爲class Bar合成一個default constructor。被合成的Bar default constructor內含必要的代碼,能夠調用class Foo的default constructor來處理member onject Bar::foo,但它並不產生任何代碼來初始化Bar::str。是的,將Bar::foo初始化是編譯器的責任,將Bar::str初始化則是程序員的責任。被合成的default constructor看起來可能像這樣:

// Bar的default constructor可能會被這樣合成
// 被member foo調用class Foo的default constructor
inline
Bar::Bar()
{
    // C++ 僞碼
    foo.Foo::Foo();
}

假設程序員經由下面的default constructor提供了str的初始化操作:

// 程序員定義的default constructor
Bar::Bar() { str = 0; }

但是編譯器還需要初始化member object foo,編譯器的行動是:“如果class A內含一個或一個以上的member class objects,那麼class A的每一個constructor必須調用每一個member class的default constructor”。編譯器會擴張已存在的constructor,在其中安插一些碼,使得user code在被執行之前,先調用必要的default constructor。擴張後的constructor可能像這樣:

// 擴張後的default constructor
// C++僞碼
Bar::Bar()
{
    foo.Foo::Foo(); //附加上的compiler code
    str = 0;        //explicit user code
}

如果有多個class member object都要求constructor初始化操作,C++語言要求以“member object在class中的聲明次序”來調用各個constructor。這一點由編譯器完成,它爲每一個constructor安插程序代碼,以“member聲明次序”調用每一個member所關聯的default constructor。這些代碼將被安插在explicit user code之前。

1.2.2 “帶有Default Constructor”的Base Class

如果一個沒有任何constructor的class派生自一個“帶有default constructor”的base class,那麼這個derived class的default constructor會被視爲nontrivial,並因此需要被合成出來。它將調用上一層base classes的default constructor(根據它們的聲明次序)。

如果設計者提供多個constructor,但其中都沒有default constructor呢?編譯器會擴張現有的每一個constructor,將“用以調用所有必要的default constructor”的程序代碼加進去。它不會合成一個新的default constructor,這是因爲其它“由user所提供的constructor”存在的緣故。如果同時亦存在着“帶有default constructor”的member class object,那些default constructor也會被調用——在所有base class constructor都被調用之後。

1.2.3 “帶有一個Virtual Function”的Class

class Widget
{
public:
    virtual void flip() = 0;
    // ...
};

void flip(const Widget& widget) { widget.flip(); }

// 假設Bell和Whistle都派生自Widget
void foo()
{
    Bell b;
    Whistle w;

    flip(b);
    flip(w);
}

下面兩個擴張操作會在編譯期間發生:

1.一個virtual function table會被編譯器產生出來,內放class的virtual functions地址;

2.在每一個class object中,一個額外的pointer member(也就是vptr)會被編譯器合成出來,內含相關的class vtbl的地址。

此外,widget.flip()的虛擬引發操作(virtual invocation)會被重新改寫,以使用widget的vptr和vtbl中的flip()條目:

//widget.flip()的虛擬引發操作(virtual invocation)的轉變
(*widget.vptr[1])(&widget)

其中:

1表示flip()在virtual table中的固定索引;

&widget代表要交給“被調用的某個flip()函數實體”的this指針。

爲了讓這個機制發揮功效,編譯器必須爲每一個Widget(或其派生類)object的vptr設定初值,放置適當的virtual table地址。對於class所定義的每一個constructor,編譯器會安插一些代碼來做這樣的事情。對於那些未聲明任何constructor的class,編譯器會爲它們合成一個default constructor,以便正確地初始化每一個class object的vptr。

1.2.4“帶有一個Virtual Base Class”的Class

Virtual base class的實現法在不同的編譯器之間有極大地差異。然而,每一種實現法的共同點在於必須使virtual base class在其每一個derived class object中的位置,能夠在運行時準備妥當。

對於class所定義的每一個constructor,編譯器會安插那些“允許每一個virtual base class的運行時存取操作”的代碼。如果class沒有聲明任何constructor,編譯器必須爲它合成一個default constructor。

 

對於沒有存在以上四種情況而又沒有聲明任何constructor的class,我們說它們擁有的是implicit trivial default constructor,它們實際上並不會被合成出來。

在合成的default constructor中,只有base class subobjects和member class objects會被初始化。所有其他的nonstatic data member,如整數、整數指針、整數數組等等都不會被初始化。這些初始化操作對程序而言或許有需要,但對編譯器則並非必要。如果程序需要一個“把某指針設爲0”的default constructor,那麼提供它的人應該是程序員。

 

2.Copy Constructor的建構操作

Default constructor和copy constructor在必要的時候才由編譯器產生出來。

這個句子中的“必要”意指當class不展現bitwise copy semantics時。

就像default constructor一樣,C++ Standard上說,如果class沒有聲明一個copy constructor,就會有隱含的聲明(implicitly declared)或隱含的定義(implicitly defined)出現。和以前一樣,C++ Standard把copy constructor區分爲trivial和nontrivial兩種。只用nontrivial的實體纔會被合成於程序之中。決定一個copy constructor是否爲trivial的標準在於class是否展現出所謂的“bitwise copy semantics”。

2.1 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”而定。

// 以下聲明展現了bitwise copy semantics
class Word
{
public:
    Word(const char*);
    ~Word() { delete [] str; }
    // ...
private:
    int cnt;
    char* str;
};

這種情況下並不需要合成出一個default copy constructor,因爲上述聲明展現了“default copy semantics”,而verb的初始化操作也就不需要以一個函數調用收場。

// 以下聲明並未展現出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也都會被複制,正如我們所期待的一樣。

2.2 不要Bitwise Copy Semantics

什麼時候一個class不展現出“bitwise copy semantics”呢?有四種情況:

1>當class內含一個member object而後者的class聲明有一個copy constructor時(不論是被class設計者明確地聲明,就像前面的String那樣;或是被編譯器合成,像class Word那樣);

2>當class繼承自一個base class而後者存在有一個copy constructor時(再次強調,不論是被明確聲明或是被合成而得);

3>當class聲明瞭一個或多個virtual functions時;

4>當class派生自一個繼承串鏈,其中有一個或多個virtual base classes時。

前兩種情況中,編譯器必須將member或base class的“copy constructor調用操作”安插到被合成的copy constructor中。3>和4>兩種情況後面展開討論。

2.2.1 重新設定Virtual Table的指針

只要一個class聲明瞭一個或多個virtual functions,編譯器就會增加一個virtual function table,並將一個指向virtual function table的指針安插在每一個class object內。當編譯器導入一個vptr到class之中時,該class就不再展現bitwise semantics了。

class ZooAnimal
{
public:
    ZooAnimal();
    virtual ~ZooAnimal();

    virtual void draw();
private:
    // ...
};

class Bear: public ZooAnimal
{
public:
    Bear();
    void draw(); // 雖未明寫virtual,它其實是virtual
private:
    // ...
};

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的vptr被設定爲Bear class的virtual table(靠編譯器安插的代碼完成)。因此,把yogi的vptr值拷貝給winnie的vptr是安全的。

當一個base class object以其derived class的object內容做初始化操作時,其vptr賦值操作也必須保證安全,

ZooAnimal franny = yogi; // 這會發生切割(sliced)行爲

franny的vptr不可以被設定指向Bear class的virtual table(但如果yogi的vptr被直接“bitwise copy”的話,就會導致此結果),而應該設定指向ZooAnimal的virtual table。

也就是說,合成出來的ZooAnimal copy constructor會明確設定object的vptr指向ZooAnimal class的virtual table,而不是直接從右手邊的class object中將其vptr現值拷貝過來。

2.2.2 處理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中做出仲裁。

class Raccoon: public virtual ZooAnimal
{
public:
    Raccoon() { ... }
    Raccoon(int val) { ... }

private:
    // ...
};

class RedPanda: public Raccoon
{
public:
    RedPanda() { ... }
    RedPanda(int val) { ... }

private:
    // ...
};

編譯器所產生的的代碼(用以調用ZooAnimal的default constructor、將Raccoon的vptr初始化,並定位出Raccoon中的ZooAnimal subobject)被安插在兩個Raccoon constructor之內,成爲其先頭部隊。

再強調一次,如果以一個Raccoon object作爲另一個Raccoon object的初值,那麼“bitwise copy”就綽綽有餘了。然而,如果企圖以一個RedPanda object作爲little_critter的初值,編譯器必須判斷“後續當程序員企圖存取其ZooAnimal subobject時是否能夠正確地執行”:

// 簡單的bitwise copy還不夠,
// 編譯器必須明確地將little_critter的
// virtual base class pointer/offset初始化

RedPanda little_red;
Raccoon little_critter = little_red;

在這種情況下,爲了完成正確的little_critter初值設定,編譯器必須合成一個copy constructor,安插一些代碼以設定virtual base class pointer/offset的初值(或只是簡單地確定它沒有被抹消),對每一個members執行必要的memberwise初始化操作,以及執行其它的內存相關工作。

在下面的情況下,編譯器無法知道是否“bitwise copy semantics”還保持着,因爲它無法知道(沒有流程分析)Raccoon指針是否指向一個真正的Raccoon object,或是指向一個derived class object:

//簡單的bitwise copy可能夠用,也可能不夠用
Raccoon *ptr;
Raccoon little_critter = *ptr;

3.成員們的初始化隊伍(Member Initialization List)

下列情況中,爲了讓程序能夠被順利編譯,必須使用member initialization list:

1>當初始化一個reference member時;

2>當初始化一個const member時;

3>當調用一個base class的constructor,而它擁有一組參數時;

4>當調用一個member class的constructor,而它擁有一組參數時。

下面的例子可以被正確編譯並執行,但是效率不高,

class Word
{
    string _name;
    int _cnt;
public:
    Word()
    {
        _name = 0;
        _cnt = 0;
    }
};

// constructor可能的內部擴張結果:
// C++ 僞碼
Word::Word( /* this pointer goes here */ )
{
    // 調用 String的default constructor
    _name.String::String();
    
    // 產生暫時性對象
    String temp = String(0);

    // "memberwise"地拷貝 _name
    _name.String::operator=(temp);

    // 摧毀暫時性對象
    temp.String::~String();

    _cnt = 0;
}

一個明顯更有效率的實現方法:

// 較佳的方式
Word::Word() : _name(0)
{
    _cnt = 0;
}

// 可能的擴張結果
// C++僞碼
Word::Word( /* this pointer goes here */ )
{
    // 調用 String(int) constructor
    _name.String::String(0);
    _cnt = 0;
}

intialization list中的項目次序是由class中的members聲明次序決定的,不是由initialization list中的排列次序決定的。

3.1 在initialization list中調用member function

// X:xfoo() 被調用,這樣好嗎
X::X(int val)
    : i(xfoo(val)), j(val)
{ }

能否調用一個member function設定一個member的初值?其中xfoo()是X的一個member function。答案是yes,但是請使用“存在於constructor體內的一個member”,而不要使用“存在於member initialization list中的member”,來爲另一個member設定初值。你並不知道xfoo()對X object的依賴性有多高,如果你把xfoo()放在constructor體內,那麼對於“到底是哪一個member在xfoo()執行時被設立初值”這件事,就可以確保不會發生模棱兩可的情況。

Member function的使用是合法的(當然我們必須不考慮它所用到的members是否已初始化),這是因爲和此object相關的this指針已經被構建妥當,而constructor大約被擴充爲:

// C++僞碼:constructor被擴充後的結果
X::X(/* this pointer, */ int val)
{
    i = this->xfoo(val);
    j = val;
}

3.2 調用member function初始化基類

如果一個derived class member function被調用,其返回值被當做base class constructor的一個參數,將會如何:

// 調用 FooBar::fval() 可以嗎
class FooBar : public X
{
    int _fval;
public:
    int fval() { return _fval; }
    
    FooBar(int val)
        : _fval(val)
        , X(fval())
    { }
    // ...
};

// 可能的擴張結果
// C++ 僞碼
FooBar::FooBar( /* this pointer goes here */ )
{
    // oops: 實在不是一個好主意
    X::X( this, this->fval() );
    _fval = val;
}

它的確不是一個好主意。

總結,編譯器會對initialization list一一處理並可能重新排序,以反映出members的聲明次序。它會安插一些代碼到constructor體內,並置於任何explicit user code之前。

4. Named Return Value(NRV)優化

在一個如bar()這樣的函數中,所有的return指令傳回相同的具名數值(named value),因此編譯器有可能自己做優化,方法是以result參數取代named return value。例如下面的bar()定義:

X bar()
{
    X xx;
    // ...處理 xx
    return xx;
}

編譯器把其中的xx以__result取代:
void
bar(X& __result)
{
    // default constructor 被調用
    // C++ 僞碼
    __result.X::X();

    // ... 直接處理 __result

    return;
}

這樣的編譯器優化操作,有時候被稱爲Named Return Value(NRV)優化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章