C++异常处理运行时是如何实现的?

93

我对C++异常处理机制的工作方式感到好奇。具体来说,异常对象存储在哪里,以及它如何在多个作用域中传播直到被捕获?它是否存储在某个全局区域中?

由于这可能是与编译器有关的,因此能否有人在g ++编译器套件的上下文中解释这一点?


5
阅读这篇文章会有所帮助。 - Ahmed
我不知道,但我猜 C ++ 规范有一个清晰的定义。(我可能错了) - Paul Nathan
2
不,规范没有给出定义。它规定行为,而不是实现。保罗,你可能想要指定你感兴趣的实现。 - Rob Kennedy
1
相关问题:https://dev59.com/RHVC5IYBdhLWcg3wZwNT - CesarB
请参考C++性能技术报告的第5.4节。 - Yogesh Arora
4个回答

58

实现可能会有所不同,但是一些基本的想法是从需求中得出的。

异常对象本身是在一个函数中创建的对象,在调用者中被销毁。因此,通常无法在堆栈上创建对象。另一方面,许多异常对象并不是很大。因此,可以创建一个32字节的缓冲区,并在需要更大的异常对象时溢出到堆。

关于实际的控制转移,存在两种策略。其中一种是在堆栈本身中记录足够的信息以展开堆栈。这基本上是一个要运行的析构函数列表和可能捕获异常的异常处理程序。当发生异常时,回溯堆栈执行那些析构函数,直到找到匹配的catch。

第二种策略将此信息移动到堆栈外的表中。现在,当发生异常时,调用堆栈用于查找已进入但未退出的范围。然后,在静态表中查找这些范围,以确定将处理抛出的异常的位置以及在其中运行哪些析构函数。这意味着堆栈上的异常开销较少;返回地址仍然需要。表是额外的数据,但编译器可以将它们放在程序的需求加载段中。


6
据我所知,g++ 编译器采用第二种地址表的方法实现 C++ 异常处理,这样做可能是为了与 C 语言兼容。而微软的 C++ 编译器则采用了一种混合方法,因为它的 C++ 异常是建立在 SEH(结构化异常处理)之上的。在每个 C++ 函数中,MSC++ 创建并注册一个 SEH 异常处理记录,该记录指向一个包含当前函数中 try-catch 块和析构函数地址范围的表格。throw 语句将 C++ 异常打包成 SEH 异常,并调用 RaiseException() 函数,然后 SEH 将控制权返回给特定于 C++ 的处理例程。 - Anton Tykhyy
1
@Anton:是的,它使用地址表方法。有关详细信息,请参阅我在另一个问题的答案https://dev59.com/RHVC5IYBdhLWcg3wZwNT#307716。 - CesarB
感谢你的回答。你可以看出C语言纯粹主义者对C++和它的异常处理可能感到惊恐。在运行时,一个简单的try/catch可能会无意中创建若干个堆栈对象或使你的程序膨胀。这就是为什么嵌入式系统通常会避免使用它们的原因。 - speedplane
2
@speedplane:不,那更多是由于缺乏理解。错误处理从来都不是免费的。C只是强迫你亲自编写它。而我们都知道有多少C程序在某些很少使用的代码路径中缺少了free()fclose() - MSalters
@MSalters我不是不同意,这几乎完全是缺乏理解。工程师经常不理解异常如何工作以及异常将如何影响他们的代码,这会导致在使用异常时产生犹豫感,这是可以理解的。如果异常处理实现更清晰地传达(并且不像魔术一样),许多人会更不犹豫地使用它们。 - speedplane
我猜了栈方法,但错过了表方法。栈是自然的,但表可能更复杂。无论哪种方式,我认为必要的部分是记录在哪里/谁来捕获异常。 - smwikipedia

22

这个定义在标准的 15.1 抛出异常中。

throw语句创建了一个临时对象,但是如何分配该临时对象的内存是未指定的。

在创建临时对象后,控制权将传递给调用堆栈中最近的处理程序,从 throw 到 catch 点之间解开堆栈。由于堆栈被解开,任何堆栈变量都会按照创建顺序相反的顺序被销毁。

除非异常被重新抛出,否则临时对象将在它被捕获的处理程序结束时被销毁。

注意:如果你使用引用方式捕获异常,那么该引用将会引用到临时对象上,如果你使用值方式捕获异常,该临时对象会被复制到值上(因此需要一个复制构造函数)。

建议来自 S.Meyers(采用 const 引用方式捕获异常)。

try
{
    // do stuff
}
catch(MyException const& x)
{
}
catch(std::exception const& x)
{
}

3
未指明的另一个问题是程序如何展开堆栈以及如何知道“最近的处理程序”所在位置。我相信Borland拥有一种实现该功能的专利方法。 - Rob Kennedy
3
被投反对:a)不是“S. Myers”,而是“Scott Meyers”;b)引用失实:“Effective C++”:“条款13:通过引用捕获异常”。这样可以对异常对象进行微调/附加信息。 - Sebastian Mach
3
不要忘记第21条规则:“尽可能使用const”。没有理由去修改异常。你应该选择a)“修复并丢弃”、b)重新抛出异常或c)生成新的异常。 - Martin York
1
@phresnel:是的,你有你的理由(我不赞同你的逻辑),我也有我的理由,虽然我不会声称已经与他们讨论过这个“具体”问题或者实际了解他们的想法(Meyers、Alexandrescu 和 Sutter),但我认为我的解释是正确的。但如果你在西雅图地区,那么你可以和他们三个交谈,因为他们经常参加 North West C++ 用户组(Meyers 没有其他人那么频繁)。 - Martin York
C++程序员为何相信标准没有强制实现的异常机制?如果您无法检查异常,又如何确保它们的行为是值得信赖的?显然,异常必须是运行时构造,因此异常对象必须在运行时可检查,但是否有标准或API可用于检查异常?如果没有,为什么我们不只是使用带有错误回调函数的函数,并默认情况下(如果没有给出回调)不会在每次抛出异常时使程序崩溃? - Dmytro
显示剩余12条评论

13
您可以在这里查看详细的解释。
此外,还可以参考C语言中使用的一种技巧来实现基本的异常处理。这需要以以下方式使用setjmp()和longjmp():前者保存堆栈以标记异常处理程序(类似于“catch”),而后者用于“抛出”一个值。 "Thrown"的值被视为从调用的函数返回。当再次调用setjmp()或函数返回时,“try块”结束。

11

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