正确替换 C++ 中缺少的“finally”

Proper replacement for the missing 'finally' in C++

提问人:Sascha 提问时间:11/20/2008 最后编辑:rlerallutSascha 更新时间:11/24/2008 访问量:6874

问:

由于 C++ 中没有,因此如果您希望代码异常安全,则必须改用 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

C 异常 最后是 C++-FAQ

评论

0赞 Martin York 11/21/2008
那是丑陋的。你应该创建一个 RunObject 或其他东西!!
0赞 Piotr Dobrogost 6/14/2010
+1 问这个问题,因为经常看到不熟悉的人认为这很好......RAIIfinally
0赞 ToolmakerSteve 11/21/2013
“您不必编写 2 次清理代码”。你为什么要出于任何原因编写两次代码?这就是子例程的用途 =P 在 RAII 与最后这个问题上保持中立,但编写两次代码是一条红鲱鱼:您可以创建一个清理例程,将任务传递给它,然后在两个地方调用它。无需代码复制。在您的简单示例中,从逻辑上讲,这是任务析构函数的一部分。但是,如果出于某种原因,您希望在 foo 中进行清理,请在那里创建一个本地清理例程。

答:

9赞 Pieter 11/20/2008 #1

我不认为有一种更干净的方法来实现你想要做的事情,但我认为你的例子中“最终方法”的主要问题是关注点的分离不当。

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

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

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

16赞 Sébastien RoccaSerra 11/20/2008 #2

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

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

另请参阅这篇 DrDobb 的文章和这篇关于 ScopeGuard 的更多信息的其他文章

评论

0赞 Jacek Sieka 7/18/2012
需要注意的一个细节是,如果清理代码本身抛出,使用 ScopeGuard 将静默丢弃抛出的异常(考虑到 C++ 的异常销毁处理,这当然是合理的) - 操作的第一个示例将终止,第二个示例将从清理代码中抛出新的异常,而不是原始异常(如果有的话)
1赞 jalf 11/21/2008 #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 默认情况下会删除指针,但您可以指定自定义删除器函数,它可以执行任意清理 takss。对于指针上的 RAII,这通常是您想要的。

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

但通常情况下,每种类型都应该知道如何自行清理。不是在引发异常时在范围内的每个对象之后(这是最终执行的,也是您尝试执行的),只是在它本身之后。如果每个对象都这样做,那么你根本不需要“在范围内的每个对象之后进行清理”这个包罗万象的功能。

评论

0赞 Johannes Schaub - litb 11/21/2008
删除了我的评论,因为我的套接字示例是 meh :)我想我同意很少有最终可以使用的情况。但无论如何,拥有/模拟它会很好:)
6赞 Martin York 11/21/2008 #4

这是一种非常丑陋的方式:(你来自Java吗?

请阅读这篇文章:
C++ 是否支持“finally”块?(我一直听说的这个“RAII”是什么?

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

2赞 Roddy 11/24/2008 #5

我通常使用更类似的东西:

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...
  }
}