為什麼說:不要使用 dynamic_cast, 需要運行時確定類型信息, 說明設計有缺陷?

在&中, google內部約定: 除單元測試外, 不要使用 dynamic_cast, 如果你需要在運行時確定類型信息, 說明設計有缺陷. 何解? 我們真的不需要dynamic_cast嗎?

PS: 網上的結論是因為dynamic_cast的轉換並不會總是成功的, dynamic_cast轉換符只能用於含有虛函數的類.

轉換後的指針判空是很正常的啊, 網上的結論完全沒有說服力.

為方便說明具個現在項目中真實的例子, 簡單抽象了一下代碼如下:

void GameObjectKillTarget(GameObject* pKiller, GameObject* Target) {
if(!pKiller.isAlive()) return;

Player* pPlayer = dynamic_cast&(pKiller);
Monster* pMonster = dynamic_cast&(Target);
if(pPlayer pMonster) {
pPlayer-&>PlayerKillMonser(pMonster);
}
//...
}

敢問如何改進?


這個問題其實一般都是學問家們來討論的,但是挺有趣,所以咱們也可以探討一下。

首先,C++ 的RTTI(包括了 dynamic_cast)肯定不是個很好的設計:

  • dynamic_cast 是有可能拋出 std::bad_cast 異常的,但大多數時候,我們不希望使用 C++ 異常系統,理由嘛,多種多樣,我的原因是——我就根本沒學會用異常這個技術。而且 C++ 異常系統是沒有 finally 關鍵字的,很彆扭。
  • C++ 的 RTTI 整體上比較弱(比如無法枚舉成員函數),幾乎就等於不能用,真要用類型信息的話,很多項目選擇自己實現。
  • dynamic_cast 性能依賴於編譯器,如果是通過字元串比較實現的,性能堪憂,尤其是繼承樹龐大的時候,dynamic_cast 可能需要執行若干次字元串比較。當然實際上我們很少需要如此關心性能。

  • 跟大多數語言不一樣,由於多繼承的存在,C++ 的類型轉換可能會改變指針的值,你大可以想像這可能造成多麼弔詭的錯誤。

進而,濫用 dynamic_cast 會帶來一些問題,比如:

  • 假如你到處使用 dynamic_cast 確認具體類型,那麼當你要增加一個子類的時候,你得修改多少地方?你不嫌麻煩嗎?不怕漏下了嗎?
  • 使用 dynamic_cast 的代碼是難於測試的,你無法通過介面確認它到底依賴於哪些具體類,測試代碼會比較複雜。並且增加了子類就要修改測試代碼。

再進而,大多數時候都是濫用:

  • C++ 為了實現上層代碼盡量不要關心具體類型,特意設計了重載、多態、模版等特性,你不用,非要自己寫代碼處理,那你為啥要用 C++ 呢?
  • 很多時候我們只是要知道對象的某種性質(比如常見的 xxx type)而不是全部類型信息,使用 dynamic_cast 獲得了全部類型信息,繼而要 include 具體類的頭文件,不符合「最小」原則。

再再進而,大多數 dynamic_cast 都可以修改代碼去掉,至少可以盡量下壓到底層,或者集中到一起方便維護。比如:

  • 《設計模式》那本書里有一個工廠方法模式,可以用來解決一些問題,比如:

    // 帶有 dynamic_cast
    void some(Base *p) {
    Derived *pd = dynamic_cast&(p);
    Partner *ptnr = nullptr;
    if (pd) {
    ptnr = new Partner4Derived(pd);
    }
    // ...
    }
    // 不帶 dynamic_cast
    void some(Base *p) {
    Partner *ptnr = p-&>getPartner(); // 返回Partner4Derived
    // ...
    }

  • 你那個例子,可以通過函數重載去掉 dynamic_cast,比如這樣:

    class Life : public GameObject {
    public:
    // ...
    virtual void kill(Life *other) = 0;
    virtual void killedBy(Player *other) = 0;
    virtual void killedBy(Monster *other) = 0;
    virtual void killedBy(Elf *other) = 0;
    // ...
    };
    class Player : public Life {
    public:
    // ...
    virtual void kill(Life *other) {
    other-&>killedBy(this);
    }
    };
    class Elf : public Life {
    //...
    };
    class Monster : public Life {
    public:
    // ...
    // 實現你的 PlayerKillMonster
    virtual void killedBy(Player *other) {
    // ...
    }
    // ...
    };
    void gameObjectKillTarget(GameObject *killer, GameObject *target) {
    // ...
    killer-&>kill(target); // kill 是虛函數
    }

    這種實現有個挺洋氣的名字叫做「double-dispatch」,其實也很彆扭,但是確實可以去掉上層調用代碼中的 dynamic_cast。

但是,你自己也做項目你知道的,拿著《設計模式》往項目里套,往往對不上號,C++ 既然打了 dynamic_cast 這個補丁,說明還是能用到的,我說幾種個人認為比較適合用 dynamic_cast 處理的情況(注意,從這裡開始我就不確定正確性了,只是個人看法,大家覺得不對可以在評論里留言糾正):

  • 處理參數協變問題。C++ 的虛函數返回值是可以跟著 this 協變的,但是參數不行。所以會有這樣的寫法(google 規範文檔里的例子):

    bool Base::equal(Base *other) = 0;
    bool Derived::equal(Base *other) {
    Derived *p = dynamic_cast&(other);
    if (!p) return false;
    // ...
    }

    這裡要想去掉 dynamic_cast 當然是可以去掉的,但是 Derived::equal 這個函數,只是利用轉型看看 other 和 this 的類型是不是一樣,不依賴別的具體類,這樣寫似乎也沒什麼問題。

  • dynamic_cast 只是一個具體實現方式,本質上是一個「判斷對象類型並做相應處理」的問題。一段代碼完成的工作,總是要做的,不是放在 A 處做,就是放在 B 處做。假設我們想盡量保持 B 的單純,那麼這項工作就可以在 A 處,反之亦然。比如類似這樣:

    bool Object::event(Event *e) {
    switch (e-&>type()) {
    case Event::Timer:
    timerEvent((TimerEvent*)e);
    break;

    case Event::ChildAdded:
    case Event::ChildPolished:
    case Event::ChildRemoved:
    childEvent((ChildEvent*)e);
    break;
    // ...
    }

    這裡的強制轉型和 dynamic_cast 其實是個差不多的東西,想要去掉當然是可以去掉的,方法類似於上面的方法二,但是那樣會讓 Event 變得比較複雜。當我們希望它只是一個簡單的屬性集,不依賴於 Object 及其任何子類,就會有這樣的實現。Object 和 Event 都有大量的子類,並且和你給出的例子不同的是,子類隨時可能大量增加,並且和基類不在一個模塊(module)中,在這種情況下,所謂的「優雅解決」很可能是繡花枕頭,能不用就別用。


dynamic_cast 基本上用於把 Base 類指針轉換為 Derived 類指針 (即 downcast)。如果程序的運行邏輯能保證給定的 downcast 一定合法,除非繼承中有用 virtual 繼承,否則 dynamic_cast 可以用 static_cast 替代,這樣沒有運行時開銷。設計合理的程序通常能保證 downcast 一定合法。例如最常見的 Foo* -&> FooImpl*,這裡 Foo 是介面類 (pure interface)。另外根據 Google C++ Style Guide 關於多重繼承的規定,virtual 繼承一定不會出現。這部分規定使得 C++ 的繼承和 Java 相同。

同時,Google C++ Style Guide 中要求避免使用 RTTI,實際中確實只有少數情況用到 RTTI 的設計是合理的,而且一般能用其他不依賴 RTTI 但稍微繁瑣一點的方式實現。


dynamic_cast 「不OOP」。純OOP是用虛函數做dispatch的。當然我沒說非要OOP,只不過與其使用dynamic_cast,我更喜歡顯式添加類型成員(通常是一個enum),然後switch(this-&>type_member), 或者更正式地使用Boost.Variant。


這個問題好贊!

好久沒遇到這麼好的問題了。

首先,附上google C++ style guide關於dynamic_cast的討論的鏈接:

http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Run-Time_Type_Information__RTTI_

google說的挺好的,我就不重複了。

我說一下我自己對這段文字的理解。

1. dynamic_cast可能會失敗,可能不安全,可能會有效率問題,但是這不是主要原因。

2. 不使用dynamic_cast的主要原因是:代碼混亂不好維護,判斷分支過多不易讀等。

3. 需要用到dynamic_cast的地方都可以通過別的(更好的)方式來解決。如下列出了幾種常見的情況:

3.1 需要一個類似於switch的判斷,對於不同的子類,做不同的事情。

此需求可以使用「多態」來實現。

3.2 需要一個類似於if的判斷,對於某一個特殊的子類,做不同的事情。

此需求可以使用「函數重載」或者「模板偏特化」來實現。

3.3 確實需要一個子類的指針,需要調用一個子類才有的方法。

一開始就使用子類的指針就好了。

剩下的一些情況,其實都有更好的解決辦法的,只需要稍微思考一下,就能想到。


沒必要盲從,在多重繼承下,就算知道一個父類指針的真實類型,安全轉成子類也要用dynamic_cast,當然你自己計算指針偏移量除外,可能又有人說不該用多重繼承,呵呵,google的webrtc項目一堆的多重繼承怎麼說,不只多重繼承,2個毫無關係的類型強轉都用了,當然對外介面是友好的。


雙分派?題主是要根據kill的主體和客體來決定那段代碼吧?搜搜雙分派有各種解法。但是如果不是極端複雜的情況還是利索點寫switch好了


沒有說一定不能用,而是需要在恰當的場合使用恰當的特性。比如:能在編譯時解決掉的問題沒必要留到運行時、能用多態搞定的事情也沒必要使用 dynamic_cast 和 typeid 等。

所以真正需要用到 dynamic_cast 的地方就不多了。除了需要在虛基類或者多繼承等較複雜的類層次結構中漫遊,對於簡單的單繼承關係,一般派生類和積累的指針偏移為0(即:基類對象和派生類對象的this指針相同),因此不需要通過 RTTI 信息來計算指針偏移。

當然,真正需要用到此類特性的時候也不需要猶豫,還是可以用的。

關於編譯器如何實現 dynamic_cast 等 RTTI 機制,可參考小文:RTTI、虛函數和虛基類的實現方式、開銷分析及使用指導

關於:

dynamic_cast 是有可能拋出 std::bad_cast 異常的

我沒記錯的話,只有對引用做 dynamic_cast 時才會拋出異常,對指針做 dynamic_cast 失敗是通過返回 null 來通知調用者的(意即:對指針做 dynamic_cast 時不會拋出任何異常)。再說 dynamic_cast 本就不該被頻繁使用,更別提將其用在引用而不是指針上了(反正我至今也未在真實生產代碼中對引用做過 dynamic_cast)。另外:合理使用異常也沒什麼不好。

C++ 的 RTTI 整體上比較弱(比如無法枚舉成員函數)

C++ 的設計哲學是不讓用戶為其用不著的特性買單,而完整的反射特性顯然如論程序員是否會用到,都可能對應用的時間和空間性能產生比較大的影響。因此 C++ 中的 RTTI 只是方便用戶進一步實現完整的反射等機制的一種手段——通過 RTTI,用戶可以自行實現自己需要的類型機制,比如反射。

這就與 C++ 中沒有提供 GC(同樣的哲學:為了不強制無需 GC 的程序員背上沉重的 GC 收集器負擔),但提供了很方便的,用於實現 GC 的介面(operator new 等等)是一個道理:讓需要的人可以方便實現,讓不需要的人不必背負額外的包袱。

dynamic_cast 性能依賴於編譯器,如果是通過字元串比較實現的,性能堪憂

對於 VC、GCC、clang 等現代編譯器來說,這早已不是問題。記得我用過的某些早期編譯器連 switch case 的優化都成問題,現在這早已成過眼雲煙。在語言中的特性優化問題其實可以算是最簡單的問題。

跟大多數語言不一樣,由於多繼承的存在,C++ 的類型轉換可能會改變指針的值

否則還要 dynamic_cast 幹嗎呢?肯定不會改變指針值的情況直接用 static_cast、或者 C cast 之類就好。

恰當地使用語言的特性才能做到利大於弊。反之,不恰當地使用任何工具都可能產生弊大於利的後果。不管這工具是異常、RTTI 、多繼承,是反射、GC、沙箱保護,還是剪刀、榔頭、螺絲刀,沒有任何例外。

因此急著否定一樣工具前,請先看看自己是否恰當正確地用好了它。不要濫用,也不要因為沒能學會好好利用而錯失了一樣優秀的工具。


我覺得如果所有的代碼都是自己寫的, 基類自己也是可以隨時修改的情況下,dynamic_cast是盡量不要使用, 用了說明那一段代碼依賴了它本不該依賴的東西, 需要重新考慮邏輯抽象是否合理。

但是我也遇到過不得不使用dynamic_cast的情況。 就是依賴了第三方的代碼。 舉個例子, QT里的QGraphicsScene有一個items()函數, 返回QGraphicsItem*的list。 我需要MyScene繼承QGraphicsScene,實現我自己想要的一些功能, 並且我也要MyItem繼承QGraphicsItem, 實現我需要的item功能。 當我在MyScene里調用QGraphicsScene提供的各種返回QGraphicsItem的介面時,我在處理這些QGraphicsItem時, 如果不用dynamic_cast, 還真沒有什麼好的辦法。 雖然QT有提供qgraphicsitem_cast專門針對這些轉換, 但那跟dynamic_cast沒有什麼根本區別。


最近遇到了這個dynamic_cast的問題。編碼規範就是統一風格的問題。沒有誰對誰錯。我覺得定這個編碼規範的人就是不想最終工程開啟啟用運行時類型信息的編譯選項。這樣可以減少一些性能開銷。同理他應該也不想你用typeid。他認為這些運行時信息可以通過良好的設計來避免。當然不表明這個運行時信息沒有用,不然java和c#反射不都白搞了么。他應該就是覺得對於c++來說這個開銷不值得。


意思是dynamic_cast 違反了oo設計原則中的里氏替換原則吧~


推薦閱讀:

c++函數如何接受數量不定的函數參數?
如何評價使用後綴樹以及CritBitTree壓縮數據的PiXiu方法?
作為一名有女(男)朋友的程序員是一種什麼體驗?
對象沒有默認構造函數,如何定義對象數組?
python 的 tuple 是不是冗餘設計?

TAG:編程 | 軟體工程 | 代碼 | C | CC |