例外狀況和錯誤處理的新式 C++ 最佳做法
在新式 C++ 中,在大部分情況下,報告及處理邏輯錯誤和執行階段錯誤的慣用方式是使用例外狀況。 當堆疊可能包含數個可偵測錯誤的函式之間的函式呼叫,以及具有處理錯誤內容的函式時,尤其如此。 例外狀況針對程式碼提供正式且妥善定義的方式,可偵測錯誤以將資訊傳遞至呼叫堆疊。
將例外狀況用於例外程式碼
程式錯誤通常分成兩個類別:
- 程式設計錯誤所造成的邏輯錯誤。 例如,「索引超出範圍」錯誤。
- 超出程式設計人員控制範圍的執行階段錯誤。 例如,「網路服務無法使用」錯誤。
在 C 樣式的程式設計和 COM 中,錯誤報告是藉由傳回值,代表特定函式的錯誤碼或狀態碼,或藉由設定呼叫端在每次函式呼叫之後選擇性擷取的全域變數來管理錯誤報告,以查看是否報告錯誤。 例如,COM 程式設計會使用 HRESULT
傳回值將錯誤傳達給呼叫端。 而 Win32 API 具有 GetLastError
函式,可擷取呼叫堆疊所報告的最後一個錯誤。 在這兩種情況下,呼叫端必須辨識程式碼並適當地回應。 如果呼叫端未明確地處理錯誤碼,程式可能會當機而不發出警告。 或者,它可能會繼續使用不正確的資料來執行,並產生不正確的結果。
在新式 C++ 中偏好使用例外狀況,原因如下:
- 例外狀況會強制呼叫程式碼來辨識錯誤狀況並加以處理。 未處理的例外狀況會使程式停止執行。
- 例外狀況會跳至可處理錯誤之呼叫堆疊中的點。 中繼函式可以讓例外狀況傳播。 它們不需要與其他階層協調。
- 根據妥善定義的規則,例外狀況堆疊回溯機制會在擲回例外狀況之後,終結範圍中的所有物件。
- 例外狀況可在偵測錯誤的程式碼與處理錯誤的程式碼之間提供清楚的區隔。
下列簡化範例示範在 C++ 中擲回和攔截例外狀況的必要語法:
#include <stdexcept>
#include <limits>
#include <iostream>
using namespace std;
void MyFunc(int c)
{
if (c > numeric_limits< char> ::max())
{
throw invalid_argument("MyFunc argument too large.");
}
//...
}
int main()
{
try
{
MyFunc(256); //cause an exception to throw
}
catch (invalid_argument& e)
{
cerr << e.what() << endl;
return -1;
}
//...
return 0;
}
C++ 中的例外狀況類似於 C# 和 JAVA 等語言的例外狀況。 在 try
區塊中,如果擲回例外狀況,則會由型別符合例外狀況的第一個相關聯 catch
區塊攔截。 換句話說,執行會從 throw
陳述式跳至 catch
陳述式。 如果找不到任何可用的 catch 區塊,則會叫用 std::terminate
且程式會結束。 在 C++ 中,可能會擲回任何型別;不過,我們建議您擲回直接或間接衍生自 std::exception
的型別。 在上一個範例中,例外狀況型別 invalid_argument
是定義於 <stdexcept>
標頭檔的標準程式庫中。 C++ 不提供或要求 finally
區塊,以確保擲回例外狀況時會釋放所有資源。 資源擷取是初始化 (RAII) 慣用語,其使用智慧型指標,可提供資源清除所需的功能。 如需詳細資訊,請參閱如何:例外狀況安全的設計。 如需 C++ 堆疊回溯機制的相關資訊,請參閱例外狀況和堆疊回溯。
基本指導方針
強固的錯誤處理在任何程式設計語言中都是一項挑戰。 雖然例外狀況提供數個支援良好錯誤處理的功能,但它們無法為您執行所有工作。 若要了解例外狀況機制的優點,請在設計程式碼時留意例外狀況。
- 使用判斷提示來檢查應一律為 true 或一律為 false 的條件。 使用例外狀況來檢查可能發生的錯誤,例如,公用函式參數的輸入驗證錯誤。 如需詳細資訊,請參閱例外狀況與判斷提示一節。
- 當處理錯誤的程式碼與一或多個介入函式呼叫偵測錯誤的程式碼分開時,請使用例外狀況。 當處理錯誤的程式碼與偵測到錯誤的程式碼緊密結合時,請考慮是否要在效能關鍵迴圈中使用錯誤碼。
- 針對可能擲回或傳播例外狀況的每個函式,提供三個例外狀況保證之一:強式保證、基本保證或 nothrow (
noexcept
) 保證。 如需詳細資訊,請參閱如何:例外狀況安全的設計。 - 依值擲回例外狀況,藉傳址方式攔截例外狀況。 請勿攔截您無法處理的內容。
- 請勿使用 C++11 中已被取代的例外狀況規格。 如需詳細資訊,請參閱例外狀況規格和
noexcept
一節。 - 適用時使用標準程式庫例外狀況型別。 從
exception
類別 階層衍生自訂例外狀況型別。 - 不允許例外狀況從解構函式或記憶體解除配置函式逸出。
例外狀況和效能
如果沒有擲回例外狀況,例外狀況機制的效能成本最低。 如果擲回例外狀況,堆疊周遊和回溯的成本大致相當於函式呼叫的成本。 其他資料結構必須在進入 try
區塊之後追蹤呼叫堆疊,且擲回例外狀況時需要更多指示來回溯堆疊。 不過,在大部分情況下,效能和記憶體使用量的成本並不重要。 例外狀況對效能的負面影響可能只在記憶體受限的系統上相當重要。 或者,在效能關鍵迴圈中,錯誤可能會定期發生,且處理錯誤及報告錯誤的程式碼之間緊密結合。 無論如何,未經分析及測量的情況下,不可能知道例外狀況的實際成本。 即使在成本可觀的罕見情況下,您也可以根據正確性增加、更容易維護性,以及完善設計例外狀況原則所提供的其他優點來權衡成本。
例外狀況與判斷提示
例外狀況和判斷提示是偵測程式中執行階段錯誤的兩種不同機制。 使用 assert
陳述式來測試開發期間的情況,如果所有程式碼都正確,則應該一律為 true 或一律為 false。 使用例外狀況處理此類錯誤沒有任何意義,因為錯誤表示程式碼中的某些內容必須修正。 它不代表程式必須從執行階段復原的情況。 assert
會在陳述式停止執行,以便您可以在偵錯工具中檢查程式狀態。 例外狀況會繼續從第一個適當的 catch 處理程序執行。 即使您的程式碼正確,請使用例外狀況來檢查執行階段可能發生的錯誤狀況,例如「找不到檔案」或「記憶體不足」。例外狀況可以處理這些狀況,即使復原只會將訊息輸出至記錄並結束程式也一樣。 一律使用例外狀況來檢查公用函式的引數。 即使您的函式沒有錯誤,您也可能無法完全控制使用者可能傳遞給它的引數。
C++ 例外狀況與 Windows SEH 例外狀況
C 和 C++ 程式都可以在 Windows 作業系統中使用結構化例外狀況處理 (SEH) 機制。 SEH 中的概念與 C++ 例外狀況中的概念類似,不同之處在於 SEH 會使用 __try
、 __except
和 __finally
建構,而不是 try
和 catch
。 在 Microsoft C++ 編譯器 (MSVC) 中,會針對 SEH 實作 C++ 例外狀況。 不過,當您撰寫 C++ 程式碼時,請使用 C++ 例外狀況語法。
如需關於 SEH 的詳細資訊,請參閱 Structured Exception Handling (C/C++)。
例外狀況規格與 noexcept
C++ 引進了例外狀況規格,以指定函式可能會擲回的例外狀況。 不過,在實務上證明例外狀況規格會有問題,且在 C++11 草稿標準中已被取代。 我們建議您不要使用 throw
例外狀況規格,但 throw()
除外,這表示函式不允許逸出例外狀況。 如果您必須使用已取代表單 throw( type-name )
的例外狀況規格,則 MSVC 支援會受到限制。 如需詳細資訊,請參閱例外狀況規格 (擲回)。 noexcept
指定名稱是在 C++11 中引進,作為 throw()
的慣用替代方案。