2011-04-14 53 views
2

我想在兩個對象之間實現交互,這兩個對象的類型是從一個公共基類派生的。有一個默認的交互,並且一旦相同類型的對象進行交互,可能會發生特定的事情。 這是使用下面的雙重分派方案實施:Double dispatch產生「隱藏虛擬功能」警告,爲什麼?

#include <iostream> 

class A 
{ 
public: 
    virtual void PostCompose(A* other) 
    { 
     other->PreCompose(this); 
    } 
    virtual void PreCompose(A* other) 
    { 
     std::cout << "Precomposing with an A object" << std::endl; 
    } 
}; 

class B : public A 
{ 
public: 
    virtual void PostCompose(A* other) // This one needs to be present to prevent a warning 
    { 
     other->PreCompose(this); 
    } 
    virtual void PreCompose(A* other) // This one needs to be present to prevent an error 
    { 
     std::cout << "Precomposing with an A object" << std::endl; 
    } 
    virtual void PostCompose(B* other) 
    { 
     other->PreCompose(this); 
    } 
    virtual void PreCompose(B* other) 
    { 
     std::cout << "Precomposing with a B object" << std::endl; 
    } 
}; 

int main() 
{ 
    A a; 
    B b; 
    a.PostCompose(&a); // -> "Precomposing with an A object" 
    a.PostCompose(&b); // -> "Precomposing with an A object" 
    b.PostCompose(&a); // -> "Precomposing with an A object" 
    b.PostCompose(&b); // -> "Precomposing with a B object" 
} 

我有一個關於這兩個代碼,不幸的是完全不同的問題:

  1. 你認爲這是一個合理的做法?你會建議不同的東西嗎?
  2. 如果我省略了前兩個B方法,則會收到編譯器警告和錯誤,最後兩個B方法隱藏了A方法。這是爲什麼?一個A*指針不應該轉換爲B*指針,還是應該這樣?

更新:我剛剛發現添加

using A::PreCompose; 
using A::PostCompose; 

使錯誤和警告消失,但爲什麼是這個必要嗎?

更新2:這裏整齊地解釋:http://www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.9,謝謝。我的第一個問題呢?對此方法有何評論?

+0

你應該真的把它分解成兩個問題。他們完全不相關。一方面你有一個查找問題,另一方面你有一個雙重調度問題,並且它們是完全不相關的。至於第一個問題:這種方法是有缺陷的。我已經提供了一個簡短的答案雙重調度(連同對查找問題的更長的答案),但我不知道它有多清楚...... – 2011-04-14 10:22:17

+0

@dribeas:謝謝!我已經問過這個問題的雙重調度部分[這裏](http://stackoverflow.com/questions/4671517/c-double-dispatch-problems)。如果你能在那裏啓發我會很棒! – lytenyn 2011-04-14 11:57:21

回答

4

雙重調度通常在C++中以不同方式實現,基類具有所有不同的版本(這使得它成爲維護的噩夢,但語言是這樣的)。試圖加倍分派的問題是,動態分派會查找您調用該方法的對象的派生類型B,但該參數的靜態類型爲A*。由於A沒有以B*作爲參數的超載,因此呼叫other->PreCompose(this)將隱式向上thisA*,並且您將在第二個參數上保留單個分派。

從實際的問題來看:編譯器爲什麼會產生警告?爲什麼我需要添加using A::Precompose指令?

原因是C++中的查找規則。然後編譯器遇到obj.member()的調用,它必須查找標識符member,並且它將從靜態類型obj開始,如果在該上下文中找不到member,它將在層次結構中向上移動並在靜態類型的基地obj

一旦找到第一個標識符,查找將停止並嘗試將函數調用與可用的重載進行匹配,並且如果調用不能匹配,則會觸發錯誤。這裏重要的一點是,如果函數調用不能匹配,那麼lookup不會在層次結構中更進一步。通過添加using base::member聲明,您將標識符member從基類帶入當前作用域。

實施例:

struct base { 
    void foo(const char *) {} 
    void foo(int) {} 
}; 
struct derived : base { 
    void foo(std::string const &) {}; 
}; 
int main() { 
    derived d; 
    d.foo("Hi"); 
    d.foo(5); 
    base &b = d; 
    b.foo("you"); 
    b.foo(5); 
    d.base::foo("there"); 
} 

當編譯器遇到表達式d.foo("Hi");靜態類型的對象是derived,和查找將檢查在derived所有成員函數,所述標識符foo位於那裏,並且查找沒有向上進行。以唯一可用過載的說法是std::string const&,編譯器將添加一個隱式轉換,因此,即使有可能是最有潛力的比賽(base::foo(const char*)derived::foo(std::string const&)該呼叫更好的匹配),這將有效地打電話:

d.derived::foo(std::string("Hi")); 

對下一個表達式d.foo(5);進行類似處理,查找開始於derived,並且發現其中存在成員函數。但是參數5不能隱式轉換爲std::string const &,即使base::foo(int)中存在完美匹配,編譯器也會發出錯誤。請注意,這是調用中的錯誤,而不是類定義中的錯誤。

當處理所述第三表達,b.foo("you");靜態類型的對象是base(請注意,實際的對象是derived,但參考的類型是base&),所以查找不會derived搜索但在base而開始。它發現兩個過載,其中一個是好的匹配,所以它會調用base::foo(const char*)b.foo(5)也是如此。

最後,雖然在最派生類中添加不同的重載隱藏底座上的過載,它不會從對象中刪除他們,所以實際上你可以調用你需要完全限定的過載調用(如果函數是虛擬的,則禁用查找並跳過動態調度),所以d.base::foo("there")將不會執行任何查找,只需將調用發送到base::foo(const char*)

如果你已經加入了using base::foo聲明的derived類,你會在basefoo的所有重載的可用過載的derived,呼叫d.foo("Hi");會考慮超載的base,並找到最好的過載base::foo(const char*);,所以它實際上將作爲d.base::foo("Hi");

執行。在許多情況下,開發商沒有總是想着在查找規則是如何工作的,以及在調用d.foo(5);失敗沒有using base::foo聲明,或更糟糕的可能是令人驚訝的,撥打d.foo("Hi");即可當達到derived::foo(std::string const &)時,它顯然是base::foo(const char*)更糟的超載。這就是爲什麼編譯器會在您隱藏成員函數時發出警告的原因之一。另一個很好的理由警告的是,在很多情況下,當你真正打算重寫虛函數,你最後可能會錯誤地更改簽名:

struct base { 
    virtual std::string name() const { 
     return "base"; 
    }; 
}; 
struct derived : base { 
    virtual std::string name() {  // missing const!!!! 
     return "derived"; 
    } 
} 
int main() { 
    derived d; 
    base & b = d; 
    std::cout << b.name() << std::endl; // "base" ???? 
} 

一個小錯誤,而試圖覆蓋成員函數name (忘記const限定符)意味着您實際上正在創建不同的功能簽名。 derived::name不是覆蓋爲base::name,因此通過對base的引用對name的調用將不會被調度到derived::name !!!

+0

謝謝,我沒有見過雙重調度問題。我會問第二個問題,說明我的雙重調度問題的具體情況,因爲不幸的是,實施基礎類中的所有內容都是不可能的。 – lytenyn 2011-04-14 10:32:54

1
using A::PreCompose; 
using A::PostCompose; 
makes the errors and warnings vanish, but why is this necessary? 

如果用相同的名稱添加新的功能,以您的派生類的基類中包含和,如果你不從基類重寫虛函數,那麼新的名稱從基地隱藏舊名稱類。

這就是爲什麼你需要明確地寫入取消隱藏:

using A::PreCompose; 
using A::PostCompose; 

取消隱藏它們(在這種特殊情況下),另一種方式是,覆蓋從基類,你所做的虛函數在你發佈的代碼中。我相信代碼會編譯得很好。

+1

是的,謝謝。其實,我剛剛找到了http://www.parashift.com/c++-faq-lite/strange-inheritance.html#faq-23.9,它可以很好地解釋它。我應該在問之前發現,對不起。 – lytenyn 2011-04-14 09:30:19

+1

@lytenyn:沒關係,沒必要說對不起:P – Nawaz 2011-04-14 09:32:53

0

類是範圍,基類中的查找被描述爲在封閉範圍內查找。

當查找函數的重載時,如果在嵌套函數中找到函數,則在封閉範圍內查找不會完成。

這兩條規則的後果就是您嘗試的行爲。添加using子句從封閉範圍導入定義,並且是常規解決方案。