2009-09-09 65 views
1

這是一些真實代碼的簡化,以及當我沒有意識到其他人已經實現Foo並從中派生出來時發生的真實錯誤。是否可以使用虛擬繼承來防止意外創建鑽石?

#include <iostream> 

struct Base { 
    virtual ~Base() { } 
    virtual void print() = 0; 
}; 

struct OtherBase { 
    virtual ~OtherBase() { } 
}; 

struct Foo : public Base { // better to use virtual inheritance? 
    virtual void print() { std::cout << "Foo" << std::endl; }; 
}; 

struct Bar : public Base { // better to use virtual inheritance? 
    virtual void print() { std::cout << "Bar" << std::endl; }; 
}; 

// The design is only supposed to implement Base once, but I 
// accidentally created a diamond when I inherited from Bar also. 
class Derived 
    : public OtherBase 
    , public Foo 
    , public Bar // oops. 
{ 
}; 

int main() { 
    Derived d; 
    OtherBase *pO = &d; 

    // cross-casting 
    if (Base *pBase = dynamic_cast<Base *>(pO)) 
     pBase->print(); 
    else 
     std::cout << "fail" << std::endl; 
} 

編輯:拯救你不必運行此代碼...

  • 如果運行原來的樣子,它打印「失敗」(不理想的,很難調試)。
  • 如果刪除標記爲「oops」的行,則會打印「Foo」(所需的行爲)。
  • 如果你離開「oops」並使兩個繼承是虛擬的,它將不會編譯(但至少你知道要修復的東西)。
  • 如果刪除「oops」並使它們變爲虛擬,它將編譯並打印「Foo」(所需行爲)。

使用虛擬繼承,結果要麼是好的,要麼是編譯器錯誤。如果沒有虛擬繼承,結果可能是好的或無法解釋的,難以調試的運行時故障。


當我實現了酒吧,基本上是複製什麼美孚已經在做,就引起了動態轉換失敗,這意味着不好的東西在真正的代碼。

起初我很驚訝沒有編譯器錯誤。然後我意識到沒有虛擬繼承,這會觸發GCC中的'沒有唯一的最終覆蓋'錯誤。我故意選擇不使用虛擬繼承,因爲在這種設計中不應該有任何鑽石。

但是當我從Base派生出來的時候使用了虛擬繼承,那麼代碼本來就可以工作(沒有我的oops),並且在編譯時我會被警告鑽石,運行。

所以問題是 - 你認爲使用虛擬繼承來防止未來犯同樣的錯誤是可以接受的嗎?在這裏使用虛擬繼承沒有很好的技術原因(我可以看到),因爲設計中永遠不應該有鑽石。它只會在那裏執行設計約束。

回答

2

不是一個好主意。

虛擬繼承僅在提前計劃時才使用。正如你剛纔發現的,所有後代類在很多情況下都必須知道它。如果基類有一個非默認構造函數,那麼您不得不擔心它始終由葉類構造的事實。

哦,除非事情自從上次查看以來發生了變化,否則無法在沒有基類幫助的情況下將虛擬基類向任何派生類下注。

2

這裏沒有鑽石!
你創建的是一個多重繼承。每個基類都有自己的基本副本。

pO有一種OtherBase *。
沒有辦法將OtherBase *的對象轉換爲Base *類型。
因此,動態轉換將返回一個NULL指針。

問題是動態轉換在運行時有一個指向Derived對象的指針。但是從這裏到達基地是一個模糊的操作,因此以NULL失敗。沒有編譯器錯誤,因爲dynamic_cast是一個運行時操作。 (你可以嘗試從任何東西轉換爲任何結果在失敗時爲NULL(或者如果使用引用時拋出))。

兩個選項:

  • 你可以dynamic_cast的,如果你施放引用雖然拋出異常。
  • 或者您可以使用在編譯時檢查鑄造的static_cast <>

檢查出來有這樣的:

struct Base 
{ 
    Base(int x): val(x) {} 
    int val; 
... 

struct Foo : public Base 
{ 
    Foo(): Base(1) {} 
.... 

struct Bar : public Base 
{ 
    Bar(): Base(2) {} 
.... 


// In main: 
    std::cout << "Foo" << dynamic_cast<Foo&>(d).val << "\n" 
       << "Bar" << dynamic_cast<Bar&>(d).val << "\n"; 


> ./a.exe 
fail 
Foo1 
Bar2 

編譯時間檢查:

std::cout << static_cast<Base*>(pO) << "\n"; // Should fail to compile. 
std::cout << static_cast<Base*>(&d) << "\n"; // Will only fail if ambigious. 
              // So Fails if Foo and Bar derived from Base 
              // But works if only one is derived. 
+0

」這裏沒有鑽石!「 - 好吧,如果我將繼承性變爲虛擬,那麼它在技術上只是鑽石。好點子。問題仍然存在:爲了防止我犯的那種錯誤,使得繼承是虛擬的嗎? 「沒有辦法將OtherBase *的對象轉換爲Base *類型 - 」 - 你在這個錯誤的,嘗試它。可以使用dynamic_cast在層次結構之間進行投射。 – Dan 2009-09-09 22:44:16

+2

@丹。它只在從Base繼承的Foo/Bar中有一個時有效。這是因爲dynamic_cast <>是一個運行時間轉換(沒有一個編譯時間信息可用(只有運行時信息))。所以動態的情況是將Derived類型的對象轉換爲類型Base。這顯然是允許的。 PS術語「跨層次鑄造」具有誤導性,您不會在標準中的任何位置找到該術語。您只需使用運行時信息來降低(或升級)一個層次結構。 – 2009-09-09 22:50:16

+0

如果必須爲基礎定義一次Derived。那麼你應該看看Boosts靜態斷言。我相信你可以找到一個方法來添加一個編譯時檢查。 – 2009-09-09 22:52:53

1

第一件事你應該考慮到這是inheritance is not for code reuse,所以想兩次從兩個基地繼承和共同的祖先和方法的實施d在雙方。

如果您認爲您確實想繼承兩種基本形式,您將希望使用虛擬繼承而不是複製祖先。這在執行exception hierarchies時很常見。請注意,虛擬基礎由最具派生類型的構造函數直接初始化,並需要關心這一點。 「