2010年2月1日 星期一

C++物件模型之一

記得十幾年前當C++正席捲整個軟體業的時候,有部分engineer拒絕從C進化到C++,主要的論點是C++ compiler在背後做太多事,以至於影響程式的效能。時至今日,C++早已成為許多軟體、系統的基本開發語言,但我覺得我自己對於compiler到底在程式碼上面動了甚麼手腳,掌握度還是不夠,於是開始研究起C++物件模型,並且想用一系列的文章和大家交流一下。




有個常見的迷思是:

  1. 如果我們沒有為一個class寫default constructor,則compiler會幫我們做一個出來。
  2. 這個default constructor會幫我們把data member給初始值


但事實上,這兩點都不正確,甚至包括destructor、copy constructor、copy assignment operator也不一定會有。

回到問題的原點,為什麼compiler會需要幫我們做一個default constructor? 答案是compiler自己需要,要了解為什麼compiler會有此需求,我們得了解一點compiler到底對我們的程式做了什麼手腳,下面的C++ code:
class X {
    int _data;
public:
    void showData() {
        cout << _data;
    }
};

int main() {
    X x;
    x.showData();
}
會被compiler變成下面的樣子,然後才編譯成機器語言
void __X_showData(X* pX) {
    cout << pX->_data;
}

int main() {
    X x;
    __X_showData(&x);
}
是否有回到C語言的感覺,class X不見了,取而代之的是一個global function,而main()當中的local變數x則會導致compiler幫我們在stack中配置一個大小為4 bytes的位置給X用來存放X的唯一data member: data,然後在呼叫__X_showData時把這個位置以指標的形式傳進去。

這種情況下compiler是不會幫我們做出default constructor的,因為不需要。那什麼情況下會需要default constructor?
class String {
    int _size;
    char* pStr;
public:
    String() {
        _size = 0;
        _pStr = 0;
    }
};

class Person {
    String _name;
    int    _age;
public:
    void showName() {
        cout << _name;
    }
};

int main() {
    Person p;
    p.showName();
}
由於String有宣告default constructor,C++語言標準要求在產生Person物件時,它的的data member: String name的default constructor必須被呼叫到,因此C++ compiler為了符合C++標準的規範,必須幫Person class做了一個default constructor,其唯一的任務就是呼叫name的default constructor:
inline
void __String_String(String* pThis) {
    pThis->_size = 0;
    pThis->_pPtr = 0;
}

inline
void __Person_Person(Person* pThis) {
    __String_String(pThis->_name);
}
void __Person_showName(Person* pThis) {
    cout << pThis->_name;
}

int main() {
    Person p;
    __Person_Person(&p);
    __Person_showName(&p);
}
值得注意的是:

  1. Compiler合成的default constructor只做它需要的事(呼叫String name的default constructor),它不會幫我們初始化像int _age這種data member,因此_age的值不會被清為0
  2. Compiler合成的default constructor會盡可能宣告成inline,因此即使Person物件位於bottleneck處,也不會因為做了default constructor而付出function call overhead的代價。
好,看過default constructor之後,來看看什麼時候compiler會需要幫我們做一個copy constructor和copy assignment operator,這部分較複雜些,需要了解C++物件模型中的virtual function機制。可曾想過為什麼下面的code中pShape->draw()可以呼叫到Circle::draw()而不是Shape::draw()?
class Shape {
public:
    virtual void draw() {...}
};
class Circle : public Shape{
    int x;
    int y;
    int radius;
public:
    virtual void draw() {...}
};

int main() {
    Circle c;
    Shape* pShape = &c;
    pShape->draw(); // Circle::draw() been called, not Shape::draw()
}
以下是C++ compiler做的手腳:
void __Shape_draw(Shape* pShape) {...}
void* __vftable_Shape[] = { __Shape_draw };
void __Shape_Shape(Shape* pThis) {
    pThis->__vfPtr = __vftable_Shape;
}

void __Circle_draw(Circle* pThis) {...}
viud* __vftable_Circle[] = { __Circle_draw };
viud __Circle_Circle(Circle* pThis) {
    pThis->__vfPtr = __vftable_Circle;
}

int main() {
    Circle c;
    __Circle_Circle(&c); // call Circle's constructor to setup Circle's __vfPtr
    Shape* pShape = &c;
    pShape->__vfPtr[0] (pShape);
}
關於virtual function,C++ compiler標準的做法是:

  1. 產生一份virtual table(__vftable_XXX),其中每個slot是指向virtual function的指標
  2. 為每個物件安插一個指標(__vfPtr),指向virtual table,並在constructor中安插程式碼幫__vfPtr初始化,指到該class的virtual table
這種情況下Circle物件在記憶體中的長相如下


















然後考慮下面的code
Circle c
Shape s1(c);
s1.draw();

Shape s2 = c;
s2.draw();
如果compiler沒有合成copy constructor或copy assignment operator,則會以bitwise copy的方式來產生s1, s2
Circle c;
Shape s1.__vfPtr = c.__vfPtr;
s1.__vfPtr[0]();  // Circle::draw() been called, which is wrong!

Shape s2.__vfPtr = c.__vfPtr;
s1.__vfPtr[0]();  // Circle::draw() been called, which is wrong!
上面的錯誤在於s1.draw()應該叫到Shape::draw(),但由於s1在產生的時候,不小心複製到Circle的virtual function pointer,而Circle的virtual table中第一個欄位當然指向Circle::draw()而不是Shape::draw()。因此compiler需要產生copy constructor和copy assignment operator來幫s1, s2設定正確的__vfPtr:
inline
void __Shape_Shape(Shape* pThis, const Shape& rhs) {
    pThis->__vfPtr = __vftable_Shape;
}
inline
void __Shape_operator::=(Shape* pThis, const Shape& rhs) {
    pThis->__vfPtr = __vftable_Shape;
}

事實上以support virtual function來看就更清楚知道為什麼compiler需要幫class產生default constructor (為了設定__vfPtr),即使我們有寫default constructor,compiler也會安插程式碼進去設定__vfPtr。

關於C++物件模型的書,我會推薦Stan Lipperman's Inside The C++ Object Model,有中譯本由侯俊傑翻譯,值得一讀!

    2 則留言: