32

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

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

  ~Foo()
  {
    UndoSomething();
  } 
} 

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

有一些明显的方法可以解决这个特定的问题,比如包装...在一个 try/catch 块中,然后调用UndoSomething(),但是 a: 那是重复的代码,而 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 现在不需要析构函数。这听起来比它的价值更麻烦,还是这已经是一种常见的模式,有一些有用的东西可以帮助我处理繁重的工作?

4

6 回答 6

31

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

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

正如 Steve Jessop 在评论中所说:如果您有多个资源要获取,那么每个资源都应该由其自己的 RAII 对象管理;并且将它们聚合为另一个类的数据成员,依次构造它们可能是有意义的。然后,如果任何一次获取失败,之前获取的所有资源都会被各个类成员的析构函数自动释放。

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

于 2012-07-05T14:20:13.537 回答
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
  }
} 
于 2012-07-05T14:22:37.790 回答
6

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

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

doer 在 ctor 主体开始之前创建,并在析构函数因异常失败或对象被正常销毁时被销毁。

于 2012-07-05T14:22:08.980 回答
6

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

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

  ~Foo()
  {
  } 

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

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

于 2012-07-05T14:25:28.700 回答
6

try/catch 通常不是代码异味,它应该用于处理错误。但是,在您的情况下,这将是代码异味,因为它不处理错误,而只是进行清理。这就是析构函数的用途。

(1) 如果在构造函数失败时应该调用析构函数中的所有内容,只需将其移至私有清理函数,由析构函数调用,在失败的情况下由构造函数调用。这似乎是你已经完成的。好工作。

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

于 2012-07-05T14:31:14.120 回答
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()永远不会运行,但你的初始化/删除对是。

一件好事是:如果您的初始化函数本身抛出,您的删除函数将不会被调用,因为初始化函数抛出的任何异常都会直接通过Trasher(),因此~Trasher()不会被执行。

注意:有一个最外层很重要try/catch,否则标准不需要堆栈展开。

于 2012-07-06T11:27:12.380 回答