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

Rod*_*ddy 31 c++ exception raii

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

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

  ~Foo()
  {
    UndoSomething();
  } 
} 
Run Code Online (Sandbox Code Playgroud)

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

有明显的方法来解决这个特定的问题,比如...在一个try/catch块然后调用UndoSomething(),但是a:那是重复的代码,而b:try/catch块是一种代码气味,我试图通过使用RAII技术来避免.而且,如果涉及多个Do/Undo对,代码可能会变得更糟,更容易出错,而且我们必须在中途进行清理.

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

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

  ~Bar()
  {
    if (f != NULL)
      f();
  }
}   
Run Code Online (Sandbox Code Playgroud)

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

class Foo
{
  Bar b;

  Foo()
  {
    DoSomething();
    b.f = UndoSomething; 
    ...     
  }
}
Run Code Online (Sandbox Code Playgroud)

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

Mik*_*our 30

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

在您的情况下,除了DoSomething()并且UndoSomething()应该由类的用户负责,而不是类本身.

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

(另外,请记住规则三 ;您的类需要防止复制,或以某种合理的方式实现它,以防止多次调用UndoSomething()).

  • 我要说的是什么.我想补充一点,一旦你编写了一个用于管理每个资源的类,如果资源组合合理,你可以将它们中的几个聚合为另一个类的数据成员.如果数据成员构造函数抛出,则已经初始化的任何成员都将被销毁. (5认同)
  • @Roddy:ITYM,"有几种资源可以获得".您可能还没有意识到他们是独立的资源,但RAII模式正在尽力告诉您:-) (5认同)

R. *_*des 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
  }
} 
Run Code Online (Sandbox Code Playgroud)

  • @Nick如果重载决策选择了该函数,则编译失败.这是一个新功能.您可以通过将其设置为私有来在不支持此功能的编译器中实现类似的功能. (2认同)

小智 6

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

class Doer
{
  Doer()
  { DoSomething(); }
  ~Doer()
  { UndoSomething(); }
};
class Foo
{
  Doer doer;
public:
  Foo()
  {
    ...
  }
};
Run Code Online (Sandbox Code Playgroud)

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


Mic*_*hne 6

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

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

  ~Foo()
  {
  } 

  private:
  class Something {
    Something() { DoSomething(); }
    ~Something() { UndoSomething(); }
  };
  Something s;
} 
Run Code Online (Sandbox Code Playgroud)

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


Moo*_*uck 6

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

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

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