C++ 栈展开
Stack Unwinding
当程序抛出一个异常时,程序暂停当前函数的执行过程并立即开始查找(look up)最邻近的与异常匹配的 catch 子句。
- 如果查找到一个匹配的 catch 子句,异常从它的抛出点开始“向上”传递到匹配的 catch 子句。异常传递过程中,当退出了某些作用域时,该作用域内异常发生前创建的局部对象会被销毁,按照与创建时相反的顺序依次销毁,对于类对象,销毁时会调用它的析构函数。上述过程称为栈展开(stack unwinding)。示例程序见 Cpp-Primer/Ch18_01_StackUnwinding.cpp at main · ltimaginea/Cpp-Primer · GitHub 。
- 如果没有查找到匹配的 catch 子句,即异常没有被捕获,程序将调用标准库函数 std::terminate ,它将终止当前的程序。默认情况下, std::terminate 会调用 std::abort 。出于底层操作系统方面的原因,当调用 std::terminate 时局部变量的析构函数是否会被调用是由具体C++实现所决定的。所以当程序因未捕获的异常而终止时,是否调用异常发生前创建的局部对象的析构函数是依赖于具体实现的(一方面,经过测试,对于 GNU g++ 9.3.0 ,执行和 gdb 调试时都不会调用析构函数;对于 Visual Studio 2022 MSVC ,“
Ctrl+F5
执行(不调试)”时不会调用析构函数,但在“F5
调试”时,当报错“未经处理的异常”时,选择“F5
继续”,结果会调用析构函数;另一方面,如果我们使用 std::set_terminate 为 std::terminate 安装新的 std::terminate_handler ,那么就有可能调用析构函数了,比如以 std::exit 替换默认的 std::abort 作为新的 std::terminate_handler ,同时如果异常发生前创建的局部变量是static
的,那么程序因未捕获的异常而终止时就会调用局部static
变量的析构函数了,示例程序见 Cpp-Primer/Ch18_01_set_terminate.cpp at main · ltimaginea/Cpp-Primer · GitHub )。
为了能够快速处理异常,编译器应该会做一定的记录工作:在每一个 try 语句块的进入点记录对应的 catch 子句能够处理的异常类型。如果发生异常,程序在运行期便可以根据记录的数据来快速查找(look up)是否存在与异常匹配的 catch 子句,从而快速处理异常。不同的编译器的具体策略会有所不同。
std::terminate : 终止当前的程序。默认情况下, std::terminate 会调用 std::abort 。当我们使用 std::set_terminate 为 std::terminate 安装新的 std::terminate_handler 时,新安装的 std::terminate_handler 最终应该终止程序,如果没有, std::abort 将会被自动调用以终止程序(经过使用 MSVC 和 g++ 测试,确实是这样。See: Unhandled C++ exceptions | Microsoft Docs )。
std::abort : 导致程序异常终止。它不进行清理工作:不会调用自动对象,静态对象和线程局部对象的析构函数。
std::exit : 导致程序正常终止。它会进行一些清理工作:会调用静态对象和线程局部对象的析构函数;但不进行栈展开(stack unwinding):不会调用自动对象的析构函数。
std::abort 和 std::exit 这两个函数都不会销毁自动对象,因为 stack unwinding 不会被执行起来。如果希望确保所有局部对象的析构函数被调用,应该运用异常机制(捕获异常)或正常返回,然后从 main() 退出程序。
Exceptions and stack unwinding in C++ | Microsoft Docs 的栈展开(stack unwinding)的描述如下:
In the C++ exception mechanism, control moves from the throw statement to the first catch statement that can handle the thrown type. When the catch statement is reached, all of the automatic variables that are in scope between the throw and catch statements are destroyed in a process that is known as stack unwinding. In stack unwinding, execution proceeds as follows:
- Control reaches the
try
statement by normal sequential execution. The guarded section in thetry
block is executed. - If no exception is thrown during execution of the guarded section, the
catch
clauses that follow thetry
block are not executed. Execution continues at the statement after the lastcatch
clause that follows the associatedtry
block. - If an exception is thrown during execution of the guarded section or in any routine that the guarded section calls either directly or indirectly, an exception object is created from the object that is created by the
throw
operand. (This implies that a copy constructor may be involved.) At this point, the compiler looks for acatch
clause in a higher execution context that can handle an exception of the type that is thrown, or for acatch
handler that can handle any type of exception. Thecatch
handlers are examined in order of their appearance after thetry
block. If no appropriate handler is found, the next dynamically enclosingtry
block is examined. This process continues until the outermost enclosingtry
block is examined. - If a matching handler is still not found, or if an exception occurs during the unwinding process but before the handler gets control, the predefined run-time function
terminate
is called. If an exception occurs after the exception is thrown but before the unwind begins,terminate
is called. In these cases, it is implementation-defined whether any stack unwinding occurs at all: throwing an uncaught exception is permitted to terminate the program without invoking any destructors. - If a matching
catch
handler is found, and it catches by value, its formal parameter is initialized by copying the exception object. If it catches by reference, the parameter is initialized to refer to the exception object. After the formal parameter is initialized, the process of unwinding the stack begins. This involves the destruction of all automatic objects that were fully constructed—but not yet destructed—between the beginning of thetry
block that is associated with thecatch
handler and the throw site of the exception. Destruction occurs in reverse order of construction. Thecatch
handler is executed and the program resumes execution after the last handler—that is, at the first statement or construct that is not acatch
handler. Control can only enter acatch
handler through a thrown exception, never through agoto
statement or acase
label in aswitch
statement.
References
- Bjarne Stroustroup's The C++ Programming Language ,Chapter 13
- Exceptions and stack unwinding in C++ | Microsoft Docs
- Unhandled C++ exceptions | Microsoft Docs
- try-block - cppreference.com
- throw expression - cppreference.com
- std::terminate - cppreference.com
- std::abort - cppreference.com
- std::exit - cppreference.com