【问题标题】:Proper replacement for the missing 'finally' in C++正确替换 C++ 中缺失的“finally”
【发布时间】:2010-09-23 05:30:01
【问题描述】:

由于 C++ you have to use the RAII 设计模式中没有 finally,因此如果您希望代码是异常安全的。一种方法是使用本地类的析构函数,如下所示:

void foo() {
    struct Finally {
        ~Finally() { /* cleanup code */ }
    } finalizer();
    // ...code that might throw an exception...
}

与直接解决方案相比,这是一个很大的优势,因为您不必编写 2 次清理代码:

try {
    // ...code that might throw an exception...
    // cleanup code (no exception)
} catch (...) {
    // cleanup code (exception)
    throw;
}

本地类解决方案的一大缺点是您无法在清理代码中直接访问本地变量。因此,如果您需要访问它们,它将使您的代码膨胀很多:

void foo() {
    Task* task;
    while (task = nextTask()) {
        task->status = running;
        struct Finally {
            Task* task;
            Finally(Task* task) : task(task) {}
            ~Finally() { task->status = idle; }
        } finalizer(task);
        // ...code that might throw an exception...
    }
}

所以我的问题是: 有没有结合这两种优势的解决方案?这样您 a) 不必编写重复的代码 b) 可以访问清理代码中的局部变量,如上一个示例中的 task,但不会出现这种代码膨胀。

【问题讨论】:

  • 那是丑陋的。你应该创建一个 RunObject 什么的!!!
  • +1 提出这个问题,因为经常看到不熟悉 RAII 的人认为 finally 很好......
  • “您不必编写 2 次清理代码”。为什么你会写两次代码,出于某种原因?这就是 =P Neutral 在 RAII 与 finally 问题上的子例程,但是编写两次代码是一个红鲱鱼:您可以创建一个清理例程,将任务传递给它,然后在两个地方调用它。无需重复代码。在您的简单示例中,这在逻辑上是任务析构函数的一部分。但是,如果出于某种原因需要在 foo 中进行清理,则在此处创建一个本地清理例程。

标签: c++ exception finally c++-faq


【解决方案1】:

我通常使用类似这样的东西:

class Runner {
private:
  Task & task;
  State oldstate;
public:
  Runner (Task &t, State newstate) : task(t), oldstate(t.status); 
  {
    task.status = newstate;
  };

  ~Runner() 
  {
    task.status = oldstate;
  };
};

void foo() 
{
  Task* task;
  while (task = nextTask())
  {
    Runner r(*task, running);
            // ...code that might throw an exception...
  }
}

【讨论】:

    【解决方案2】:

    这是一种丑陋的做法:(你来自 Java 吗?)

    请阅读这篇文章:
    Does C++ support 'finally' blocks? (And what's this 'RAII' I keep hearing about?)

    它解释了为什么 finally 是一个如此丑陋的概念以及为什么 RIAA 更加优雅。

    【讨论】:

      【解决方案3】:

      正如其他人所说,“解决方案”是更好的关注点分离。 在您的情况下,为什么任务变量不能自行清理? 如果需要对其进行任何清理,那么它不应该是一个指针,而是一个 RAII 对象。

      void foo() {
      //    Task* task;
      ScopedTask task; // Some type which internally stores a Task*, but also contains a destructor for RAII cleanup
          while (task = nextTask()) {
              task->status = running;
              // ...code that might throw an exception...
          }
      }
      

      在这种情况下,您可能需要智能指针(默认情况下,boost::shared_ptr 将删除指针,但您可以指定自定义删除器函数,它可以执行任意清理任务。对于指针上的 RAII,这通常是你会想要的。

      问题不在于缺少 finally 关键字,而是您使用了无法实现 RAII 的原始指针。

      但通常,每种类型都应该知道如何清理自己。不是在抛出异常时范围内的每个对象之后(这是最终所做的,以及您试图做的),只是在其自身之后。如果每个对象都这样做,那么您根本不需要“在作用域中的每个对象后清理”功能。

      【讨论】:

      • 删除了我的评论,因为我的套接字示例是 meh :) 我认为我同意最终可以使用的情况很少。但无论如何拥有/模拟它会很好:)
      【解决方案4】:

      您可以在 Task 类的函数中提取清理代码,而不是定义 struct Finally,并使用 Loki 的 ScopeGuard

      ScopeGuard guard = MakeGuard(&Task::cleanup, task);
      

      有关 ScopeGuard 的更多信息,另请参阅 DrDobb's articleother article

      【讨论】:

      • 需要注意的一个细节是,如果清理代码本身抛出异常,使用 ScopeGuard 会默默地丢弃抛出的异常(考虑到 C++ 的异常销毁处理,这当然是合理的) - 操作的第一个示例将终止,第二个将从清理代码中抛出新异常,而不是原始异常(如果有)
      【解决方案5】:

      我认为没有更简洁的方法来实现您想要做的事情,但我认为您示例中“最终方法”的主要问题是不正确的separation of concerns

      例如函数 foo() 负责 Task 对象的一致性,这不是一个好主意,Task 本身的方法应该负责将状态设置为合理的。

      我确实意识到有时确实需要 finally,而您的代码显然只是一个简单的示例来说明一个观点,但这种情况很少见。在极少数情况下,我可以接受更多做作的代码。

      我想说的是,您应该很少需要 finally 构造,并且对于您这样做的少数情况,我想说不要浪费时间构建更好的方法。它只会鼓励你 finally 使用比你真正应该使用的更多......

      【讨论】:

        猜你喜欢
        • 2020-01-04
        • 2021-09-24
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-05-12
        相关资源
        最近更新 更多