防御 scope_exit RAII 类型中的异常处理Defending against exceptions in a scope_exit RAII type
微软资深工程师 Raymond Chen 探讨了在 C++ 中使用 scope_exit 类时如何应对构造函数或析构函数中可能抛出的异常。他分析了多种场景下的最佳实践,强调即使资源清理操作本身不应失败,也必须为其编写健壮的错误处理逻辑,以确保程序稳定性。
Raymond Chen
Windows 实现库 (WIL) 中有一个便捷的辅助工具是 wil::scope_exit。我们用它来模拟其他语言中的 finally 关键字,即在控制离开作用域时执行代码。
在使用 scope_exit 时,我发现了三个可能发生异常的地方。
auto cleanup = wil::scope_exit([captures] { action; });第一个是在 lambda 的构造过程中。如果在捕获变量的初始化期间发生异常会怎样?
这种异常在 scope_exit 被调用之前就已经发生了,因此 scope_exit 对此无能为力。异常会向外传播,而清理操作永远不会执行。
另一个是在 scope_exit 试图将 lambda 移动到清理阶段时。在一个朴素的 scope_exit 实现中,异常会向外传播,而清理操作永远不会执行。
第三个是在 scope_exit 被析构时。此时抛出的异常来自析构函数。由于析构函数默认 noexcept,这默认会导致 std::terminate。如果你显式启用可抛出异常的析构函数,那么接下来会发生什么取决于析构函数运行的原因。如果它是因为正常离开块而运行,则异常会向外传播;但如果它是由于其他异常导致的栈展开而运行,则会触发 std::terminate。
危险的部分是前两种情况,因为在这两种情况下,异常会被抛出(并可能被其他地方捕获),而清理操作却从未执行。
WIL 通过简单声明:如果在复制/移动 lambda 期间发生异常,则行为是未定义的,从而解决了这个问题。
C++ 的实验性 scope_exit 以不同的方式处理该问题:如果在捕获构造期间发生异常,则在传播异常之前会先调用 lambda。(它无法处理 lambda 自身构造期间的异常,并且如果 lambda 抛出异常,也会将其行为声明为未定义。)
实际上,构造或复制时的异常问题并不重要,因为 lambda 通常通过引用 [&] 捕获所有值,而这些类型的捕获在构造或复制时不会抛出异常。
作者
Raymond 参与 Windows 的发展已有 30 多年。2003 年,他创建了一个名为 The Old New Thing 的网站,其受欢迎程度远超他的想象,这一发展至今仍让他感到不安。该网站催生了一本同名书籍《The Old New Thing》(Addison Wesley, 2007)。他偶尔会在 Windows Dev Docs Twitter 账号上出现,讲述一些毫无用处的故事。
需要完整排版与评论请前往来源站点阅读。