2010年2月6日 星期六

C++物件模型之二

今天要上一道小菜: constructor initialization list

概念很簡單,以下的constructor寫得不好:
class Person {
    String _name;
    int    _age;
public:
    Person() {
        _name = 0;
        _age = 0;
    }
};
因為compiler會把它轉化成以下(*):
Person() {
    String tmpString(0);    // temporary String object been created
    String _name.String();
    _name = tmpString;      // copy assignment operator been called
    tmpString.~String();    // destruct temporary String object

    _age = 0;    // basic data type, set value directly
}

可以看到,一個暫時的String物件被建立,再複製給實際的data memer(_name),然後再呼叫這個暫時物件的destructor(**)。

可以想像如果Person class被大量create,有不少時間會花在暫時物件上。比較好的寫法是用initialization list:
Person()
 : _name(0) {
    _age = 0;
}
這樣寫比較好是因為compiler會把它轉化成:
Person() {
    _name.String(0);    // only one String constructor called
    _age = 0;
}

知道了以上事實後,可以破除一項迷思: 什麼東西都要放到initialization list? 你可能看過以下的code:
class X {
    int a;
    bool b;
    char c;
    double d;
public:
    X()
     : a(0), b(false), c(0), d(0.0)
    {}
};
這樣的寫法除了看起來比較有學問一點之外,也只是讓compiler辛苦一點,要幫我們再轉成原本就應該寫的樣子:
X() {
    a = 0;
    b = false;
    c = 0;
    d = 0.0;
}

接下來,使用initialization list還有個要注意的地方,初始化的順序。例如下面的class初始化的結果會是錯的:
class Y {
    bool _AllowToDrink;
    int  _age;
public:
    Y(int age)
     : _age(age), _AllowToDrink(_age >= 18 ? true : false)
    {}
};
因為data member初始化的順序是以宣告的順序為主,而不是以initialization list中的順序。所以這個例子中的_AllowToDrink的值會先被evaluate,但此時_age還沒給初值,所以_AllowToDrink的結果是未知的。

這個例子的問題解法有二,一個是改變data member宣告的順序,先宣告_age再宣告_AllowToDrink。不過比較好的解法是把_AllowToDrink的初始動作放到constructor的本體中:
Y(int age)
 : _age(age) {
    _AllowToDrink = _age >= 18 ? true : false;
}
這個解法利用compiler會把initialization list中的data member轉化到constructor本體的最前面這個原則:
Y(int age) {
    _age = age;    // compiler produced code, guarantee to be placed before user code.

    _AllowToDrink = _age >= 18 ? true : false;
}
如此,無論_age與_AllowToDrink的宣告順序為何,都可以正確被賦予初值了。


* 忽略this指標等轉化以強調今天的主題
** 為什麼Compiler要做這樣轉化呢? 雖然programmer想的是要把_name物件做初始化,但寫出來的code的語意(semantic)卻是要做copy assignment (_name = 0;),而且雖然型別不對(把int硬塞給給String去吃),但compiler還會很雞婆的幫我們把整數0做轉型成String (如果String有int為參數的constructor的話,否則會出compile time error)。為了消除這樣的誤會,因此C++才會訂出constructor initialization list這樣的語法。

3 則留言: