如何处理RAII的构造函数失败

时间:2012-07-05 14:13:40

标签: c++ exception raii

我熟悉RAII的优点,但我最近在这样的代码中遇到了问题:

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

  ~Foo()
  {
    UndoSomething();
  } 
} 

一切都很好,除了构造函数...部分中的代码引发了异常,结果是UndoSomething()从未被调用过。

有一些明显的方法来解决这个特定的问题,比如在{/ 1}}的try / catch块中包裹...,但是a:那是重复的代码,b:try / catch块是我尝试使用RAII技术避免的代码味道。而且,如果涉及多个Do / Undo对,代码可能会变得更糟,更容易出错,而且我们必须在中途清理。

我想知道有更好的方法可以做到这一点 - 也许一个单独的对象需要一个函数指针,并在它被反演时调用该函数?

UndoSomething()

我知道不会编译,但它应该显示原则。 Foo然后变成......

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

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

请注意,foo现在不需要析构函数。这听起来比它的价值更麻烦吗,或者这已经是一种常见的模式,有助于我处理繁重的工作?

6 个答案:

答案 0 :(得分:30)

问题是你的班级试图做太多。 RAII的原理是它获取资源(在构造函数中或稍后),析构函数释放它;该类仅用于管理该资源。

在您的情况下,DoSomething()UndoSomething()以外的任何内容都应由班级用户负责,而不是班级本身。

正如Steve Jessop在评论中所说:如果你有多个资源需要获取,那么每个资源都应该由自己的RAII对象管理;将这些聚合为另一个依次构造每个类的数据成员可能是有意义的。然后,如果任何收购失败,所有先前获得的资源将由各个班级成员的析构者自动释放。

(另外,请记住Rule of Three;您的班级需要阻止复制或以合理的方式实施,以防止多次调用UndoSomething()。)

答案 1 :(得分: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
  }
} 

答案 2 :(得分:6)

我也会用RAII来解决这个问题:

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

doer是在ctor体启动之前创建的,当析构函数通过异常失败或者对象被正常销毁时会被破坏。

答案 3 :(得分:6)

你的班级太多了。将DoSomething / UndoSomething移动到另一个类('Something'),并将该类的对象作为Foo类的一部分,因此:

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

  ~Foo()
  {
  } 

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

现在,在调用Foo的构造函数时调用DoSomething,如果Foo的构造函数抛出,则UndoSomething会被正确调用。

答案 4 :(得分:6)

try / catch一般来说代码气味,应该用来处理错误。在你的情况下,它会是代码味道,因为它不处理错误,只是清理。这就是破坏者的用途。

(1)如果构造函数失败时应该调用析构函数中的所有,只需将其移动到私有清理函数(由析构函数调用)和构造函数(如果失败) 。这似乎就是你已经完成的事情。干得好。

(2)一个更好的想法是:如果有多个do / undo对可以单独破坏,那么它们应该被包装在他们自己的小RAII类中,这就是它的微型任务,并在它自身之后进行清理。我不喜欢你当前给它一个可选的清理指针函数的想法,这只是令人困惑。清理应始终与初始化配对,这是RAII的核心概念。

答案 5 :(得分:0)

经验法则:

  • 如果您的班级是手动管理某些内容的创建和删除,那就太多了。
  • 如果您的班级手动编写了副本分配/构建,则可能管理太多
  • 此例外:一个唯一目的是管理一个实体

第三条规则的示例包括std::shared_ptrstd::unique_ptrscope_guardstd::vector<>std::list<>scoped_lock,当然还有{ {1}}以下课程。


附录

你可以走得那么远,写点东西与C风格的东西互动:

Trasher

输出:

#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 永远不会运行,但你的init / delete对是。

一个好处是:如果你的init函数本身抛出,你的delete函数就不会被调用,因为init-function抛出的任何异常直接通过~Foo(),因此Trasher()不会被执行。

注意:最重要的是~Trasher(),否则标准不需要堆栈展开。