當 Constructor 遇上 Virtual Function
Posted on March 3rd, 2010 at 0:19 by fr3@K

Background

不知道你有沒有過這樣的經驗 - 想要在父類的 constructor 裏面呼叫被子類 override (或是 implement) 的 (pure) virtual function?

C++:
  1. struct Base
  2. {
  3.   Base()
  4.    : x(0)
  5.   {
  6.     on_create();
  7.   }
  8.   virtual void on_create() {}
  9.   int x;
  10. };
  11. struct Derived : Base
  12. {
  13.   Derived()
  14.    : y(1) {}
  15.   void on_create()
  16.   {
  17.     // do Derived specific things
  18.   }
  19.   int y;
  20. };
  21.  
  22. void foo()
  23. {
  24.   Derived d;
  25. }

我試過. 跟我一樣這樣玩過的朋友, 很快就會學到它不會如你所期望的那樣運作. 如果你的期望是當建構 Derived 的 object instance 時, Derived::on_create 會被呼叫. 要是 Base::on_create 宣告成 pure virtual 的話, 你更會發現它甚至 不能 compile.

這樣的結果或許不是那麼直覺, 但背後的原因卻非常簡單 - virtual function 的 override 是發生在 object instance 被建構的時候.

      === mini tutorial ===

      Class Derived 的 object layout 長得像下面這樣 (p 為指向一 Derived 的 object 的指標):

             +---------+      +-----------------------------+
      p----->|   vptr  |----->| ptr to typeinfo for Derived |
             +---------+      +-----------------------------+
             |    x    |      |      Derived::on_create     |
             +---------+      +-----------------------------+
             |    y    |
             +---------+
      

      由於擁有 virtual function 的原因, Derived 的 object instance 在記憶體中會有一個指向其 vtable (virtual table) 的 vptr (virtual pointer).

      === end mini tutorial ===

Behind the Scenes

打開你的 Inside the C++ Object Model 複習一下. 當我們在程式中建構一個 Derived 的 object instance 時, 先被建構的是其父類 - 也就是 class Base 的部份 - 而後, 才會建構屬於 class Derived 的部份. 這過程可約略分為幾個階段:

  1. 準備 Base
  2. 起始 Base 的 member, 也就是 Base::x
  3. 呼叫 Base 的 constructor
  4. Base 部份建構完成
  5. 準備 Derived
  6. 起始 Derived 的 member, 也就是 Derived::y
  7. 呼叫 Derived 的 constructor
  8. Derived 完整建構

許多人可能不曉得 object 的建構還有上述階段一與階段五的 "準備" 工作. 這裡說的準備, 說穿了其實就是設定 object 的 vptr! 也就是說一直到程式完成步驟五之前, 這個 object 的 vptr 指向的是屬於 class Base 的 vtable:

       +---------+      +-----------------------------+
p----->|   vptr  |----->|   ptr to typeinfo for Base  |
       +---------+      +-----------------------------+
       |    x    |      |        Base::on_create      |
       +---------+      +-----------------------------+
       |    y    |
       +---------+

完成了步驟五, 這個 object 的 vptr 才會改而指向 class Derived 的 vtable. Derived::on_create 正是在這個時候 override 了 Base::on_create:

       +---------+      +-----------------------------+
p----->|   vptr  |--+   |   ptr to typeinfo for Base  |
       +---------+  |   +-----------------------------+
       |    x    |  |   |        Base::on_create      |
       +---------+  |   +-----------------------------+
       |    y    |  |
       +---------+  |   +-----------------------------+
                    +-->| ptr to typeinfo for Derived |
                        +-----------------------------+
                        |      Derived::on_create     |
                        +-----------------------------+

因此, 在例中 Base 的 constructor (也就是表中的步驟三) 參考到的 on_create 不會是被 Derived override 的版本.

With a Little Twist

前面提到如果 Base::on_create 被宣告成 pure virtual, 任何嘗試建構 Derived 的程式將不能成功編譯.

因為在這情況下, (假裝這樣的程式可以 compile 並運行) 在開始步驟三時 object 的 layout 會是像這樣:

       +---------+      +-----------------------------+
p----->|   vptr  |----->|   ptr to typeinfo for Base  |
       +---------+      +-----------------------------+
       |    x    |      |    <pure virtual marker>    |
       +---------+      +-----------------------------+
       |    y    |
       +---------+

Compiler 會在 class Base 的 vtable 內, 原本紀錄著 Base::on_create 的欄位標上一個特殊的 marker 以表示該 virtual function 為 pure virtual. 既然 pure virtual function 是不能被使用者以有意義的方式參考, 間接呼叫了 pure virtual function 的 Derived 的 constructor (Derived ctor -> Base ctor -> pure virtual) 當然也是無效的.

What About Destructors

在 destructor 內呼叫 (pure) virtual function 也有完全一樣的問題. 只要把文中 object 建構的步驟反過來就可以理解其中的原因.

Good night.

del.icio.us:當 Constructor 遇上 Virtual Function digg:當 Constructor 遇上 Virtual Function spurl:當 Constructor 遇上 Virtual Function newsvine:當 Constructor 遇上 Virtual Function furl:當 Constructor 遇上 Virtual Function Y!:當 Constructor 遇上 Virtual Function 黑米共享書籤:當 Constructor 遇上 Virtual Function 推推王:當 Constructor 遇上 Virtual Function
Previous Post
« [Update] boost::decay documentation issue «
Next Post
» Quiz on C++ Object Model »

4 Comments »

Comment #11895

我忘了之前在哪本書看過的,反正結論說,就是不要在 constructor 使用 virtual function 就對了。

Comment by av — March 3, 2010 @ 1:37


Comment #11923

可能是這裡:Effective C++, 3rd: Item 9: Never call virtual functions during construction or destruction.

Comment by Keiko — March 3, 2010 @ 10:00


Comment #11924

好文章. 期待 “當 Destructor 遇上 Virtual + Multi-thread”

Comment by Ricky Lung — March 3, 2010 @ 10:12


Comment #12003

我習慣用 proxy (handle) class 包裝 class hierarchy 來解這個問題。因為 client code 只能接觸到 proxy (handle),所以可以隔絕採用 Virtual Constructor Idiom 的 (醜陋) implementation class hierarchy。另外,proxy (handle) class 視情況,可以用 shared_ptr<FooImplBase> 來盛裝 implementation object,以兼顧 thread-safety。

最後,Base-from-Member Idiom 是這個問題的另外一種變形,稍加修改設計,亦可適用。

Comment by jeffhung — March 4, 2010 @ 18:35


Comments RSS TrackBack URI

XHTML: You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>