2012-05-22 17 views
2

因此,我們正在研究範圍守衛或類似機制的使用,以確保傳入/傳出對象有效性和/或內部狀態不變,類似於C#代碼合同。使用範圍守衛作爲代碼合同

在正常處理過程中出現意外情況/異常而導致某些對象處於不一致狀態的特定情況下,我們可以/應該使用什麼機制來支持這一事實:範圍防護當我們跳出這個函數時會抱怨嗎?

下面是一些示例僞代碼來說明我的觀點:

struct IObjectValidator; 

struct ObjectValidatorScopeGuard 
{ 
    ObjectValidatorScopeGuard(IObjectValidator * pObj) 
    : m_ptr(pObj) 
    { 
    Assert(!m_ptr || m_ptr->isValid()); 
    } 

    ~ObjectValidatorScopeGuard() 
    { 
    Assert(!m_ptr || m_ptr->isValid()); 
    } 
    private: 
    IObjectValidtor * m_ptr; 
}; 


int SomeComponent::CriticalMethod(const ThingA& in, ThingB& inout, ThingC * out) 
{ 
    ObjectValidatorScopeGuard sg1(static_cast<IObjectValidator *>(&in)); 
    ObjectValidatorScopeGuard sg2(static_cast<IObjectValidator *>(&inout)); 
    ObjectValidatorScopeGuard sg3(static_cast<IObjectValidator *>(out)); 

    // create out 
    try 
    { 
    out = new ThingC(); 
    out->mergeFrom(inout, out); // (1) 
    } 
    catch (const EverythingHasGoneHorriblyWrongException& ex) 
    { 
    // (2) out and inout not guaranteed valid here.. 
    } 
    return 0; 
} 

所以,如果東西出了問題(1),導致「出」或「INOUT」是在一個糟糕的狀態在點(2)範圍守衛sg2/sg3將會拋出異常......而這些異常可能會掩蓋真正的原因。

是否有任何模式/約定來處理這種情況?我們是否錯過了明顯的東西

回答

6

如果由對象驗證程序保護的代碼塊發生異常,C++運行庫將調用terminate。你不能像析構函數那樣拋出異常,而正在處理其他異常。因此,您不應該從析構函數中拋出異常(details here)。您應該使用斷言或記錄錯誤,而不是拋出異常。

比檢查不變式更好的是保證它們永遠不會被破壞。這就是所謂的exception safety。基本的異常安全性(保留不變量)通常很容易通過巧妙的語句重新排序以及使用RAII來實現。的異常安全技術

例子:

class String { 
    char *data; 

    char *copyData(char const *data) { 
    size_t length = strelen(rhs->data); 
    char *copy = new char[length]; 
    memcpy(data, rhs->data, length); 
    return data; 
    } 

public: 
    ~String() { delete[] data; } 

    // Never throws 
    void swap(String &rhs) { std::swap(data, rhs->data); } 

    // Constructor, Copy constructor, etc. 
}; 

// No exception safety! Unusable! 
String &String::operator = (String const &rhs) { 
    if(&rhs == this) return *this; 

    delete[] data; 
    data = copyData(rhs->data); // May throw 
} 

// Weak exception safety 
String &String::operator = (String const &rhs) { 
    if(&rhs == this) return *this; 

    delete[] data; 
    data = 0; // Enforce valid state 
    data = copyData(rhs->data); // May throw 
} 

// Strong safety 1 - Copy&Swap with explicit copy 
String &String::operator = (String const &rhs) { 
    String copy(rhs);// This may throw 
    swap(copy);// Non-throwing member swap 
    return *this; 
} 

// Strong safety 2 - Copy&Swap with pass by value 
String &String::operator = (String rhs) { 
    swap(rhs);// Non-throwing member swap 
    return *this; 
} 
+0

D'oh!愚蠢的我忘了沒有拋出dtors :-)我們的ASSERT宏實際上也拋出,很明顯,我們正在咆哮錯誤的樹。任何用於確定正確的「聰明的語句重新排序」的指針? – JBRWilkinson

+0

+1,範圍警衛是爲了維護狀態,而不是斷言。另外,由於'Assert'實際上並沒有在這種情況下拋出任何東西,所以你可以使用常規的'assert'宏。 – Potatoswatter

+0

@JBRWilkinson:我現在無法在互聯網上找到一個例子,所以我剛剛添加了一個簡單的例子。 –

2

有趣把斷言在範圍後衛。這不是通常的用例,但提高覆蓋率並不是一個壞主意。

請注意,當您已經處理一個異常時,您不能再拋出異常。因此,in,outinout的問題無法委託給其他地方,您需要立即處理。

如果您只想在違反斷言(預期行爲爲Assert)時打印調試消息,則只需打印該消息並繼續前進......不要混淆異常。

如果Assert應該綁定到一個更大的異常處理機制中,那麼異常對象應該具有適應任何Assert實際產生的結構。 但是將該狀態變爲適當的異常對象並不重要。 Assert在堆棧展開期間,在處理異常之前,在通過重新拋出即(try { throw; } catch (structured_e &) {})可訪問之前被調用。您需要一個線程局部變量來存儲當前的結構化異常,由structured_e::structured_e()初始化。長話短說,我的建議是提供一個單獨的WeakAssert用於不引發異常的析構函數和範圍警衛。

另請參閱Herb Sutter's article爲什麼組合異常和析構函數時不要太聰明。