【问题标题】:How to handle constructor failure for RAII如何处理 RAII 的构造函数失败
【发布时间】:2012-07-05 23:09:43
【问题描述】:

我熟悉 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 现在不需要析构函数。这听起来比它的价值更麻烦,还是这已经是一种常见的模式,在 boost 中为我处理繁重的工作提供了有用的东西?

【问题讨论】:

标签: c++ exception raii


【解决方案1】:

经验法则:

  • 如果您的班级手动管理某些内容的创建和删除,则说明它做得太多了。
  • 如果您的班级手动编写了复制分配/构造,则可能管理过多
  • 例外情况:一个仅用于管理一个实体的类

第三条规则的示例是std::shared_ptrstd::unique_ptrscope_guardstd::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 很重要,否则标准不需要堆栈展开。

【讨论】:

    【解决方案2】:

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

    (1) 如果析构函数中的everything应该在构造函数失败时调用,只需将其移到一个私有的清理函数中,由析构函数调用,在失败的情况下由构造函数调用.这似乎是你已经完成的。干得好。

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

    【讨论】:

    • 谢谢。同意重新。例外——但我在 catch 语句中看到了太多的清理代码,所以我仍然怀疑它们。这个概念是清理函数是“可选的”,以避免在调用 DoSomething 之前抛出异常时调用 UndoSomething。 void Foo() : b(UndoSomething) { ... DoSomething());
    • @Roddy:如果清理是在小类中,那么初始化也应该在小类中,这不是问题。
    • 听起来像void Foo() : b(DoSomething, UndoSomething) 是必需的。有趣...
    • 或者更好的是,让它们成为b 的成员,而不是像所有其他建议的答案那样传递函数指针。
    • 是的,这可能是最好的方法。这种模式在与非 C++ 代码交互时出现了很多,我正在考虑有一个将 'do' 和 'undo' 参数作为 boost::function 类型(或者是 boost::bind - 不确定)的类避免编写大量这些小类。
    【解决方案3】:

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

    在您的情况下,DoSomething()UndoSomething() 以外的任何事情都应该是类用户的责任,而不是类本身。

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

    (另外,请记住Rule of Three;您的类需要防止复制,或者以某种合理的方式实现它,以防止多次调用UndoSomething())。

    【讨论】:

    • 我要说什么。我要补充一点,一旦你编写了一个类来管理每个资源,你可以将其中的几个聚合在一起作为另一个类的数据成员,如果资源组合是有意义的。如果数据成员构造函数抛出,那么任何已经初始化的成员都会被销毁。
    • 可以,但是资源的获取涉及到几个步骤:DoSomething是获取资源的第一步。
    • @Roddy: ITYM,“有几个资源可以获取”。您可能还没有意识到它们是独立的资源,但 RAII 模式正在尽最大努力告诉您:-)
    • @Steve,好的,所以 DoSomething/UndoSomething 是“第一个”资源——这似乎意味着它应该有一个单独的 RAII 对象来管理它——就像 C.Stoll 建议的那样。
    • 回复。三法则。为了清楚起见,示例中省略了 BOOST_NONCOPYABLE ... ;-)
    【解决方案4】:

    只需将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
      }
    } 
    

    【讨论】:

    • @Nick 如果重载决议选择了该函数,它会使编译失败。这是一个新功能。通过将其设为私有,您可以在不支持此功能的编译器中实现类似的功能。
    【解决方案5】:

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

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

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

    【讨论】:

      【解决方案6】:

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

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-01-03
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-10-01
        • 2011-01-21
        • 1970-01-01
        相关资源
        最近更新 更多