2012-07-05 92 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,否则,堆栈放卷不是标准所要求的。