1. 程式人生 > 其它 >C++ 棧展開

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_terminatestd::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_terminatestd::terminate 安裝新的 std::terminate_handler 時,新安裝的 std::terminate_handler 最終應該終止程式,如果沒有, std::abort 將會被自動呼叫以終止程式(經過使用 MSVC 和 g++ 測試,確實是這樣。See: Unhandled C++ exceptions | Microsoft Docs )。

std::abort : 導致程式異常終止。它不進行清理工作:不會呼叫自動物件,靜態物件和執行緒區域性物件的解構函式。

std::exit : 導致程式正常終止。它會進行一些清理工作:會呼叫靜態物件和執行緒區域性物件的解構函式;但不進行棧展開(stack unwinding):不會呼叫自動物件的解構函式。

std::abortstd::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:

  1. Control reaches the try statement by normal sequential execution. The guarded section in the try block is executed.
  2. If no exception is thrown during execution of the guarded section, the catch clauses that follow the try block are not executed. Execution continues at the statement after the last catch clause that follows the associated try block.
  3. 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 a catch clause in a higher execution context that can handle an exception of the type that is thrown, or for a catch handler that can handle any type of exception. The catch handlers are examined in order of their appearance after the try block. If no appropriate handler is found, the next dynamically enclosing try block is examined. This process continues until the outermost enclosing try block is examined.
  4. 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.
  5. 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 the try block that is associated with the catch handler and the throw site of the exception. Destruction occurs in reverse order of construction. The catch handler is executed and the program resumes execution after the last handler—that is, at the first statement or construct that is not a catch handler. Control can only enter a catch handler through a thrown exception, never through a goto statement or a case label in a switch statement.

References