C++ 棧展開
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