2011-08-28 91 views
3

可以說我有一個抽象的基類,它帶有一個返回昂貴對象的純虛函數。由於它是一個昂貴的對象,我應該返回一個參考。C++協方差和引用

但是生活並不那麼簡單,比方說我有兩個從它派生出來的類:一個具有經常調用的函數,因此在實例中存儲一個副本並返回一個引用會更有效。另一個很少被調用,所以最好根據需要創建對象來保存RAM。

我以爲我可以使用協變,因爲Liskov替代原則會很開心,但當然Obj不是Obj&的子類型,所以會導致編譯錯誤。在

class abc 
{ 
public: 
    virtual BigObj& obj() = 0; 
}; 

class derived : public abc 
{ 
public: 
    ... 
    virtual BigObj obj() { return obj_; } 

private: 
    BigObj obj_; 
}; 

結果:

conflicting return type specified for ‘virtual BigObj derived::obj()’ 

是否有一個更優雅的解決方案,這不是簡單地挑選至少最糟糕的?

+1

簡單的答案是*你不想這樣做*。你可能會認爲你想要這個,但是你並不想這麼做。返回值或引用的語義變化太大,不能從用戶代碼中隱藏*。 –

回答

4

一種解決方案是創建一個智能指針類來管理BigObj* S:

class BigObjPtr { 
public: 
    BigObjPtr(bool del, BigObj* ptr) : del(del), ptr(ptr) { } 

    BigObj* operator->() { 
     return ptr; 
    } 

    virtual ~BigObjPtr() { 
     if (del) delete ptr; 
    } 

private: 
    BigObj* ptr; 
    bool del; 
}; 

然後改變你的類返回的其中之一,而del布爾設置是否要BigObjPtr摧毀它的指針當它超出範圍:

class abc 
{ 
public: 
    virtual BigObjPtr obj() = 0; 
}; 

class derived : public abc 
{ 
public: 
    ... 
    BigObjPtr obj() { return BigObjPtr(false, &obj_); } 

private: 
    BigObj obj_; 
}; 

class otherderived : public abc 
{ 
public: 
    ... 
    BigObjPtr obj() { return BigObjPtr(true, new BigObj); } 
}; 

你會當然需要管理的BigObjPtr S等複製,但我留給你。

+1

你和Benjamin Lindley很出色,很好的回答。 – cmannett85

+0

模式'if(del)delete del;'標示冗餘:缺省'delete'在空指針上定義良好。只要刪除del就行了。 –

+0

@Konrad它不是'if(del)delete del;'它是'if(del)delete ptr;'所以我不檢查'ptr'是否爲'NULL'。所以它不是在做你認爲它是:) –

2

改爲返回shared_ptr<BigObj>。一類可以保留自己的副本,另一類可以按需創建。

1

你有兩個選擇:

  1. 正如你提到的,您可以選擇最最差。也就是說,爲這兩個派生類選擇BigObjBigObj&

  2. 您可以將新方法添加到具有適當返回類型的派生類。例如,BigObj& obj_by_ref()BigObj obj_by_val()

你不能兩者兼得的原因是因爲你可能有直接的指針abc。它指定了BigObj&,因此無論哪個類提供實現,最好返回BigObj&,因爲這就是調用網站所期望的。如果一個行爲不當的子類直接返回了BigObj,那麼當編譯器試圖將其用作參考時,會造成混亂!

0

在大多數情況下,通過引用返回是很危險的,因爲它可能導致很難跟蹤內存問題,例如,當父對象超出範圍或被刪除時。我將重新設計BigObj作爲一個簡單的委託(或容器)類,它實際上持有指向昂貴對象的指針。

+0

除非你追蹤所有權('shared_ptr'),否則一個指針不會給你更多的安全性:當父對象超出作用域時,調用析構函數並且(希望)釋放它所擁有的指針。 –

3

你應該重新考慮你的假設,函數的接口應該根據語義是什麼來定義的。所以主要問題是函數的語義是什麼?

如果您的功能創建了一個對象,無論副本多大或多小,都應該返回值,無論代碼被調用的頻率如何。另一方面,如果你正在做的是提供對已經存在的對象的訪問,那麼你應該返回對該對象的引用,在這兩個的情況下。

注意的是:

expensive function() { 
    expensive result; 
    return result; 
} 
expensive x = function(); 

可以由編譯器被優化成單個expensive對象(它可避免從result複製到返回的對象和來自的Elid返回的對象複製到x)。

關於Liskov替代原則,你不是在這裏跟隨,一種類型是返回一個對象,另一種是返回一個對象的引用,這在許多方面是完全不同的東西,所以即使你可以將類似的操作應用於兩個返回的類型,事實是有其他操作不相同,並且有不同的職責傳遞給調用者。

例如,如果您修改返回對象的引用的情況下,要更改的值都在將來函數返回的對象,而在值情況下的對象是調用者所有並且調用者對其副本執行的操作無關緊要,下一次對該函數的調用將返回新創建的對象而不進行這些更改。

所以再次,認爲你的函數的語義是什麼,並用它來定義在所有派生類中返回什麼。如果您不確定這段代碼是多麼昂貴,那麼您可以回來一個簡單的用例,我們可以討論如何提高應用程序的性能。爲此,您需要明確用戶代碼如何處理對象,以及它對函數的期望。

+0

有趣的一點!我應該給予維基百科的LSP頁面比粗略瀏覽更多... – cmannett85