使用try-catch-rethrow的代码与不使用try-catch的代码等效吗?

23

以下情况下,以下两个代码不等价?

{
  // some code, may throw and/or have side effects
}

try {
  // same code as above
} catch(...) {
  throw;
}

编辑:为了澄清,我对(i)违背上述模式的偏差(例如,在 catch 块中使用更多代码),以及(ii)邀请关于正确使用 try-catch 块的言论不感兴趣。

我正在寻找一份符合 C ++ 标准的合格答案。这个问题是由 Cheers and hth. - Alf 在评论 我的这个答案 时提出的,他在没有进一步解释的情况下声明上述代码是不等价的。


编辑:它们确实不同。后者将执行堆栈展开,但前者不一定会在运行时找到异常处理程序(堆栈中的某个 catch 块)。


它们不等价的情况有吗? - eerorika
1
@user2079303 这正是我的问题。 - Walter
哦,我明白了。我只看到了那个问题,问什么时候它们不等价,这意味着存在这样的情况。我想知道@Cheersandhth.-Alf指的是什么。 - eerorika
@user2079303 我也是。 - Walter
6个回答

17
后者需要堆栈展开,而前者如果堆栈展开则是实现定义。 相关的标准引用(均来自N3337): [except.ctor]/1:当控制从throw表达式传递到处理程序时,会为进入try块之后构造的所有自动对象调用析构函数。这些自动对象按照它们完成构造的相反顺序被销毁。 [except.ctor]/3:在从try块到throw表达式路径上调用自动对象的析构函数的过程称为“堆栈展开”。[...] [except.terminate]/2:当异常处理机制无法找到throw异常的处理程序时,将调用std::terminate() (18.8.3)。在找不到匹配处理程序的情况下,在调用std::terminate()之前是否展开堆栈是实现定义。[...] 因此,如果您想保证在未处理的异常情况下运行您的自动对象的析构函数(例如某些持久存储必须在销毁时变异),那么try {/*code*/} catch (...) {throw;} 将实现该功能,但{ /* code */ }不会。

但是编译器如何确定是否会找到异常处理程序,比如从调用函数中? - Walter
2
@Walter 我使用clang在不同的翻译单元中进行了测试,{/*code*/}没有调用析构函数,但是重新抛出版本确实调用了。我对异常实现的了解还不够,无法说明其原理。 - TartanLlama
1
@Walter:决定是否可以找到异常处理程序的不是编译器,而是运行时系统。由于在堆栈展开期间调用的析构函数无法更改将被激活的处理程序(如果有),因此系统可以自由地(我认为这就是[except.terminate]/2的含义)在搜索处理程序时进行展开和析构。但是,另一种情况是它可能首先完成搜索,只有在找到处理程序后才开始展开。在后一种情况下,如果没有找到处理程序,将根本不会发生任何破坏。 - Marc van Leeuwen
@MarcvanLeeuwen 谢谢,非常有趣。依赖于 terminate() 的问题在于错误消息可能会丢失(或者是否有其他保证)。这就是为什么我将所有代码放在一个最外层的 try-catch 块中(捕获 std::exception),报告任何消息(exception::what())。然而,一些编译器确实实现了从 terminate() 报告的代码。 - Walter
@Walter:如果在没有catch的情况下栈没有被卸载,可能是因为卸载操作与处理程序相关联。在流行(几乎无处不在)的零成本异常模型中,所有异常处理/堆栈卸载代码都由编译器存储在不同的代码/数据中(这就是在没有异常的情况下实现零成本的方式),因此,为了知道哪些析构函数需要执行,运行时很可能需要相应的条目。 - Matthieu M.
如果没有任何东西可以捕获异常,直接转到terminate()的具体理由(编译器术语中的“[two-phase unwind]”)是因为在std :: terminate上设置调试器断点将在抛出原始throw时看到堆栈。 - zwol

7

关于Cheers and hth. - Alf's comment的详细说明:

来自http://en.cppreference.com/w/cpp/error/terminate:

当C++运行时处理异常失败时,将调用std::terminate(),原因可能是以下之一:

1)抛出异常但未捕获(在这种情况下,是否进行任何堆栈展开取决于实现,不是规定的

因此,如果您的

{
  // some code, may throw and/or have side effects
}

不在另一个try/catch块中。

示例:

struct A {
    A() {}
    ~A() { std::cout << "~A()" << std::endl; }
};

int main()
{
//    try {
        A a;
        throw 1;
//    } catch(...) {
//        throw;
//    }
}

在coliru的gcc 5.2.0下使用-O2,不会打印~A(),但使用try/catch会打印。

更新: 关于你关于分离编译单元的编辑,我刚刚用我的本地gcc 4.8.2进行了测试,行为是相同的:如果没有catch,则不会进行堆栈展开。具体例子:

a.h:

struct A {
   A();
   ~A();
};

void foo();

a.cpp:

#include <iostream>
using namespace std;

struct A {
   A() {}
   ~A() { cout << "~A()" << endl; }
};

void foo() {
    A a;
    throw 1;
}

main.cpp:

#include "a.h"

int main () {
   //try {
    foo();
   //} catch(...) {
   //  throw;
   //}
}

我认为是否有catch在运行时确定,因为无论何时在运行时抛出异常,程序都需要寻找catch。因此,在运行时选择是否展开堆栈是有意义的。


啊,所以在异常根本没有被捕获的情况下情况就不同了。您能否进一步阐述一下当程序无论如何都将终止时堆栈展开的用处? - eerorika
1
@user2079303,显然需要进行任何清理代码。这里有一个快速的例子:你有一个远程服务器,每次只能接受一个连接。在你的程序中会有一些Connection对象,它的析构函数将关闭连接,以便服务器准备接受新的连接。 - Petr
3
关闭一个文件输出流,以便所有缓冲的数据实际上都写入磁盘。 - Petr

3

从语义上讲是等效的。但我不确定是否有些编译器无法优化掉不必要的try-catch。我更喜欢省略try-catch块。通常这会使代码更易于理解。


你能否引用标准来支持你的说法(代码是等价的)? - Walter
@Walter请注意,我写了“语义上等效”。据我所知,标准并没有明确说明这一点,但从异常的语义和异常处理的描述中可以明显看出。 - cdonat

3
假定“一些代码”没有出现未定义行为(在这种情况下,无论您是否添加 try/catch 块,所有赌注都将关闭),最终结果将没有区别。从技术上讲,如果未捕获异常,堆栈展开是否会发生是实现定义的(即实现必须记录其所做的操作),但迄今为止尚未有任何实现报告不展开堆栈的情况。如果发生堆栈展开,则所有本地变量将超出范围,并且具有析构函数的变量将调用析构函数。 与“一些代码”执行前设置、捕获异常(如果有)和重新抛出以及任何其他清理相关的开销可能会导致可测量的性能差异。该差异将取决于编译器,并且在旧编译器中潜在地非常显着。对于现代编译器,如果有任何开销差异,那么由于异常和异常处理的实现技术得到了改进,差异会稍微小一些。

我不确定你回答了我的问题。编译器无法确定这些代码是否在try-catch块内,那么它如何避免堆栈展开呢? - Walter
1
@Walter编译器可以生成代码来遍历(而不是解开)调用堆栈以查找异常处理程序,如果找不到则相应地采取行动。 - jepio
@jepio你的评论是迄今为止最接近令人满意答案的(还请参阅我对原始问题的最新编辑)。-- 你能把它变成一个答案吗? - Walter
我回答了你的问题,然后你编辑了你的帖子。所以暗示我没有回答你的问题是歪曲事实。然而,在标准中也没有阻止一个实现(技术上可以包括完整的工具链,而不仅仅是编译器和库)检查你的完整程序,并在它能够检测到抛出异常但从未被捕获的情况下调用std::terminate()并且不展开堆栈。但是,正如我所说,没有报道过实现这种事情的案例。 - Peter
@Peter,感谢您的回答(我已经点赞了)。您在评论中所说的基本上就是jepio所说的,这很有趣——我之前并不知道。 - Walter

1
如果你捕获基本的异常,它们完全相同。只有在抛出之前执行一些操作(如记录日志)时,才能从捕获和重新抛出异常中受益。但是你不应该捕获 Exception。只捕获你知道如何恢复的异常。

1
没有捕获到"基本异常"。你可能在想C++之外的语言的特性。 - Peter
@PalleDue 不是的。如果他们只捕获基本异常,那就不完全一样了。问题中的代码捕获了所有抛出的对象。 - eerorika
我一直在回答这个问题,以为它是C#。在编辑之前没有任何提示它是C++。对此感到抱歉。 - Palle Due
1
@PalleDue 提示在标签中。 - eerorika

-1

如果资源没有按照 RAII 惯用法进行管理,那么在重新抛出之前可以在 catch 块中执行一些有意义的清理工作。

       {
          // some code, may throw and/or have side effects
        }

        try {
          // same code as above
        } catch(...) {
//Some meaningful clean up can be performed here if resources not managed as RAII idiom
          throw;
        }

这并没有回答问题。 - Walter
@Walter,我只是试图指出一个情况,即这两个代码结构有可能存在差异。 - NightFurry
@Walter 好的,明白了!如果找到答案会回复您。 - NightFurry

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,