2012-07-05 89 views
29

我熟悉RAII的優點,但我最近絆倒了一個問題,像這樣的代碼:如何處理構造失敗的RAII

class Foo 
{ 
    public: 
    Foo() 
    { 
    DoSomething(); 
    ...  
    } 

    ~Foo() 
    { 
    UndoSomething(); 
    } 
} 

一切都很好,除了在構造...節代碼拋出異常,結果UndoSomething()從未被調用過。

有固定的特定問題,就像在一個try/catch塊,然後調用UndoSomething()包裝...的明顯的方式,而是:這是複製代碼,和b:try/catch塊是一個代碼味道,我嘗試避免使用RAII技術。而且,如果涉及多個Do/Undo對,代碼可能會變得更糟且更容易出錯,並且我們必須清理一半。

我想知道有一個更好的方法來做到這一點 - 也許一個單獨的對象需要一個函數指針,並且當它反過來被破壞時調用該函數?

class Bar 
{ 
    FuncPtr f; 
    Bar() : f(NULL) 
    { 
    } 

    ~Bar() 
    { 
    if (f != NULL) 
     f(); 
    } 
} 

我知道不會編譯,但它應該顯示原理。 Foo然後變成...

class Foo 
{ 
    Bar b; 

    Foo() 
    { 
    DoSomething(); 
    b.f = UndoSomething; 
    ...  
    } 
} 

請注意,foo現在不需要析構函數。這聽起來像是比它的價值更麻煩,還是這已經是一種常見的模式,有助於處理我的繁重工作?

+3

try/catch是_not_代碼味道,並經常被使用IMO。 – 2012-07-05 14:20:44

+2

看這裏:http://www.parashift.com/c++-faq-lite/selfcleaning-members.html – MadScientist 2012-07-05 14:22:11

+2

@MooingDuck:的確,他們本身並沒有味道。但是'try {} catch(...){throw;}'有相當強烈的氣味。 – 2012-07-05 14:56:49

回答

30

問題是你的班級正在嘗試做太多。 RAII的原則是它獲得一個資源(在構造函數中,或者在後面),並且析構函數釋放它;該類僅用於管理該資源。

在你的情況下,DoSomething()UndoSomething()以外的任何東西都應該是該類用戶的責任,而不是類本身。正如Steve Jessop在評論中所說:如果你有多種資源需要獲取,那麼每個資源都應該由它自己的RAII對象來管理;將它們作爲另一個類的數據成員進行聚合可能是有意義的,這些類依次構造每個類。然後,如果任何獲取失敗,所有先前獲得的資源將由各個類成員的析構函數自動釋放。 (另外,請記住Rule of Three;您的課程需要防止複製或以一種合理的方式實施,以防止多次致電UndoSomething())。

+5

我要說什麼。我會補充說,一旦你編寫了一個類來管理每個資源,如果資源的組合合理,你可以將它們中的幾個聚合爲另一個類的數據成員。如果數據成員構造函數拋出,則任何已經初始化的成員都將被銷燬。 – 2012-07-05 14:22:37

+0

是的,但資源的獲取涉及到幾個步驟:'DoSomething'是獲取資源的第一步。 – Roddy 2012-07-05 14:24:14

+5

@Roddy:ITYM,「有幾種資源可以獲得」。你可能還沒有意識到它們是獨立的資源,但RAII模式正在盡最大努力告訴你:-) – 2012-07-05 14:25:24

6

我想解決這個使用RAII,太:

class Doer 
{ 
    Doer() 
    { DoSomething(); } 
    ~Doer() 
    { UndoSomething(); } 
}; 
class Foo 
{ 
    Doer doer; 
public: 
    Foo() 
    { 
    ... 
    } 
}; 

行爲人是在構造函數體開始之前創建和被打爛既可以當析構函數通過異常失敗或對象時通常銷燬。

17

只是要DoSomething/UndoSomething到合適的RAII手柄:

struct SomethingHandle 
{ 
    SomethingHandle() 
    { 
    DoSomething(); 
    // nothing else. Now the constructor is exception safe 
    } 

    SomethingHandle(SomethingHandle const&) = delete; // rule of three 

    ~SomethingHandle() 
    { 
    UndoSomething(); 
    } 
} 


class Foo 
{ 
    SomethingHandle something; 
    public: 
    Foo() : something() { // all for free 
     // rest of the code 
    } 
} 
+0

什麼是'= delete'? – Nick 2012-07-05 20:40:38

+2

@Nick如果通過重載解析選擇該函數,則編譯失敗。這是一項新功能。在不支持此功能的編譯器中,您可以通過將其設置爲私有來實現類似的功能。 – 2012-07-05 20:45:20

6

你有你的一類太多。移動的DoSomething/UndoSomething到另一個類(「東西」),並有一個類的對象類Foo的一部分,正是如此:

class Foo 
{ 
    public: 
    Foo() 
    { 
    ...  
    } 

    ~Foo() 
    { 
    } 

    private: 
    class Something { 
    Something() { DoSomething(); } 
    ~Something() { UndoSomething(); } 
    }; 
    Something s; 
} 

現在,DoSomething的已被稱爲時間Foo的構造函數被調用,如果Foo的構造函數拋出,那麼UndoSomething會被正確調用。

6

try/catch是不是代碼味道一般,應該用來處理錯誤。就你而言,這將是代碼味道,因爲它不處理錯誤,僅僅是清理。那是什麼析構函數。

(1)如果在構造失敗一切在析構函數應該被調用,只需將其移動到一個私人的清理功能,它是由析構函數調用,並在發生故障的情況下,構造函數。這似乎是你已經完成的。做得好。 (2)一個更好的想法是:如果有多個do/undo對可以單獨破壞,他們應該被包裝在他們自己的小RAII類中,這是它的小事,然後自行清理。我不喜歡你給它一個可選的清理指針函數的當前想法,這只是令人困惑。清理應始終與初始化配對,這是RAII的核心概念。拇指

+0

謝謝。同意。例外 - 但我在catch語句中看到了很多清理代碼,所以我仍然以懷疑的態度對待它們。 概念是清理函數是'可選的',以避免調用UndoSomething,如果在DoSomething被調用之前拋出異常。 'void Foo():b(UndoSomething){... DoSomething());' – Roddy 2012-07-05 14:39:06

+0

@Roddy:如果清理是在miniclass中,初始化也應該在miniclass中,這是一個非問題。 – 2012-07-05 15:30:14

+0

聽起來像'void Foo():b(DoSomething,UndoSomething)'是必需的。有趣的... – Roddy 2012-07-05 16:39:10

0

規則:

  • 如果類手工管理的東西創建和刪除,這是做得太多。
  • 如果你的類有手動編寫拷貝賦值/ - 建造,這大概管理了太多
  • 例外這樣的:具有管理只有一個實體

實例的唯一目的的類第三條規則是std::shared_ptr,std::unique_ptr,scope_guard,std::vector<>,std::list<>,scoped_lock,當然還有下面的Trasher類。


附錄。

你可以走這麼遠,寫的東西與C風格的東西互動:

#include <functional> 
#include <iostream> 
#include <stdexcept> 


class Trasher { 
public: 
    Trasher (std::function<void()> init, std::function<void()> deleter) 
    : deleter_(deleter) 
    { 
     init(); 
    } 

    ~Trasher() 
    { 
     deleter_(); 
    } 

    // non-copyable 
    Trasher& operator= (Trasher const&) = delete; 
    Trasher (Trasher const&) = delete; 

private: 
    std::function<void()> deleter_; 
}; 

class Foo { 
public: 
    Foo() 
    : meh_([](){std::cout << "hello!" << std::endl;}, 
      [](){std::cout << "bye!" << std::endl;}) 
    , moo_([](){std::cout << "be or not" << std::endl;}, 
      [](){std::cout << "is the question" << std::endl;}) 
    { 
     std::cout << "Fooborn." << std::endl; 
     throw std::runtime_error("oh oh"); 
    } 

    ~Foo() { 
     std::cout << "Foo in agony." << std::endl; 
    } 

private: 
    Trasher meh_, moo_; 
}; 

int main() { 
    try { 
     Foo foo; 
    } catch(std::exception &e) { 
     std::cerr << "error:" << e.what() << std::endl; 
    } 
} 

輸出:

hello! 
be or not 
Fooborn. 
is the question 
bye! 
error:oh oh 

所以,~Foo()從來沒有運行,但你的init /刪除對是。

一個好處是:如果你的初始化函數本身拋出,你的刪除功能將不會被調用,如由init函數拋出的異常會直接通過Trasher()因此~Trasher()不會被執行。

注意:重要的是有一個最外面的try/catch,否則,堆棧放卷不是標準所要求的。