2014-08-28 112 views
5

我目前正在研究多態類型和賦值操作之間的相互作用。我主要關心的是某人是否可能嘗試將基類的值分配給派生類的對象,這會導致問題。檢測基類的派生指向派生類

this answer我瞭解到,基類的賦值運算符總是被隱式定義的派生類的賦值運算符隱藏起來。所以對於賦值給一個簡單的變量,不正確的類型會導致編譯器錯誤。

class A { public: int a; }; 
class B : public A { public: int b; }; 
int main() { 
    A a; a.a = 1; 
    B b; b.a = 2; b.b = 3; 
    // b = a; // good: won't compile 
    A& c = b; 
    c = a; // bad: inconcistent assignment 
    return b.a*10 + b.b; // returns 13 
} 

分配的這種形式可能會導致inconcistent對象的狀態,但沒有編譯器警告和代碼看起來爲非作惡對我來說是第一次:但是,如果通過引用發生轉讓,這是不正確的一目瞭然。

是否有任何成熟的習慣用法來檢測此類問題?

我想我只能希望運行時檢測,如果我發現這樣一個無效的任務會拋出異常。我現在可以想到的最佳方法是基類中的用戶定義的assigment運算符,它使用運行時類型信息來確保this實際上是指向基本實例的指針,而不是派生類,然後做一個手動的逐個成員的副本。這聽起來像是很多開銷,嚴重影響了代碼的可讀性。有什麼更容易嗎?

編輯:由於某些方法的適用性似乎取決於我想要做什麼,下面是一些細節。

我有兩個數學概念,說ringfield。每個領域都是一個環,但不是相反。每個實現有幾個實現,它們共享公共基類,即AbstractRingAbstractField,後者從前者派生。現在我嘗試實現易於編寫的基於std::shared_ptr的引用語義。所以我的Ring類包含一個std::shared_ptr<AbstractRing>持有它的實現,和一堆轉發到該方法。我想寫FieldRing繼承,所以我不必重複這些方法。特定於某個字段的方法只需將指針投射到AbstractField,我想這樣做是靜態地進行投射。我可以確保指針在施工時實際上是AbstractField,但我擔心有人會將Ring分配給Ring&,這實際上是Field,因此打破了我對所包含的共享指針的假定不變性。

+0

這裏不是真正的問題,你有一個非抽象的基類嗎? – 2014-08-28 11:01:29

+1

個人而言,我禁用了多態類型的複製構造函數和賦值運算符。繼承基礎多態性實際上不能很好地處理值類型。 – 2014-08-28 11:10:17

+0

@OliCharlesworth:我不明白。想想如何一個從按鈕派生的切換按鈕,我可以看到基類應該可實例化的情況,這個問題可能出現在現實世界中。因此我不會遵循任何「所有基礎都必須抽象」的方法。如果這不是你想到的,那麼請詳細說明抽象基類如何能夠幫助我解決問題。 – MvG 2014-08-28 11:10:21

回答

1

由於無法在編譯時檢測到向下轉換類型引用的分配,因此我會建議一個動態解決方案。這是一個不尋常的情況,我通常會反對這一點,但可能需要使用虛擬賦值運算符。

class Ring { 
    virtual Ring& operator = (const Ring& ring) { 
     /* Do ring assignment stuff. */ 
     return *this; 
    } 
}; 

class Field { 
    virtual Ring& operator = (const Ring& ring) { 
     /* Trying to assign a Ring to a Field. */ 
     throw someTypeError(); 
    } 

    virtual Field& operator = (const Field& field) { 
     /* Allow assignment of complete fields. */ 
     return *this; 
    } 
}; 

這可能是最明智的做法。

另一種方法是創建一個模板類,以便跟蹤這個並簡單地禁止使用基本指針*和引用&。模板化的解決方案可能會更難以正確實施,但可以進行靜態類型檢查來禁止沮喪。這是一個基本的版本,至少對於我來說,使用GCC 4.8和-std = C++ 11標誌(對於static_assert),正確地給出了一個編譯錯誤,「noDerivs(b)」是錯誤的來源。

#include <type_traits> 

template<class T> 
struct CompleteRef { 
    T& ref; 

    template<class S> 
    CompleteRef(S& ref) : ref(ref) { 
     static_assert(std::is_same<T,S>::value, "Downcasting not allowed"); 
     } 

    T& get() const { return ref; } 
    }; 

class A { int a; }; 
class B : public A { int b; }; 

void noDerivs(CompleteRef<A> a_ref) { 
    A& a = a_ref.get(); 
} 

int main() { 
    A a; 
    B b; 
    noDerivs(a); 
    noDerivs(b); 
    return 0; 
} 

如果用戶首先創建了自己的引用並將其作爲參數傳遞,則該特定模板仍可能被愚弄。最終,防止用戶做愚蠢的事情是一種無望的努力。有時你只能做出公正的警告,並提供詳細的最佳實踐文檔。

+0

有趣。 「在運行時無法檢測到向下類型引用的分配」:在C++ 11中,可以在'Ring :: operator =(...)'實現內執行'typeid(* this)== typeid(Ring)' ,這是我想到的。不確定虛擬運營商是否有更好或更差的性能,將不得不在一天內進行測試。 「這是java中的標準分配方式。」但是Java有引用語義,所以我沒有看到你在這裏指的是什麼類型的分配。陣列成員的分配接近,但仍然看起來不同於我。 Java方面可能不是主題,但我很好奇。 – MvG 2014-08-28 13:15:38

+0

@MvG typeid比較是在運行時使用RTTI完成的。虛擬分配使用類虛擬表並且不需要比較。一般來說,虛擬成員在C++中是首選,但我不確定RTTI是否會產生相當大的開銷。虛擬呼叫是單向指針間接尋址,而RTTI可能涉及更多(完全不確定)。 我的java有點生疏。我相信我的意思是Object .equals和.clone,而不是賦值,但也涉及到類似的語義。 – Zoomulator 2014-08-28 13:25:43

+0

我剛剛意識到我寫了運行時,當我的意思是編譯時! – Zoomulator 2014-08-28 13:36:57