C++/WinRT 的錯誤處理

本主題討論使用 C++/WinRT 程式設計時處理錯誤的策略。 關於更一般的資訊與背景,請參見 「錯誤與異常處理(現代 C++)」。

避免捕捉及擲回例外

我們建議您繼續撰寫 例外安全的程式碼,但盡量避免捕捉並拋出例外。 如果沒有例外處理程序,Windows 會自動產生錯誤報告(包括當機的迷你備份),這會幫助你找出問題所在。

不要丟出你預期能抓到的例外。 而且不要用例外來解釋預期失敗。 只有在發生非預期的執行階段錯誤時才拋出例外,其他情況都應直接在最接近失敗來源的地方以錯誤碼/結果碼處理。 這樣一來,當 出例外時,你就知道原因要麼是程式碼中的錯誤,要麼是系統中的異常錯誤狀態。

想像存取 Windows 登錄檔的情境。 如果你的應用程式無法讀取登錄檔的某個值,那是正常的,你應該要優雅地處理。 不要擲回例外;而應回傳 boolenum 值,用以表示該值未被讀取,以及可能的原因。 另一方面,若未 將數值寫 入登錄系統,則很可能表示問題比你在申請中能理性處理的更嚴重。 在這種情況下,你不希望應用程式繼續運作,因此產生錯誤報告的例外是最快避免應用程式造成損害的方法。

舉另一個例子,考慮從呼叫 StorageFile.GetThumbnailAsync 取得縮圖影像,然後將該縮圖傳給 BitmapSource.SetSourceAsync。 如果這串呼叫導致你傳給 nullptrSetSourceAsync (圖片檔案無法讀取;也許它的副檔名看起來像是包含圖片資料,但實際上沒有),那麼你會拋出一個無效的指標例外。 如果你在程式碼中發現這樣的情況,與其將該情況當作例外來攔截並處理,不如改為檢查 GetThumbnailAsync 是否回傳 nullptr

拋出例外通常比使用錯誤碼慢。 如果你只有在致命錯誤發生時才拋出例外,那麼一切順利時,你永遠不會支付性能成本。

但更可能的效能損失是執行時開銷,確保在極不可能拋出例外時能呼叫適當的結構函數。 這種保證的代價,不論是否真的拋出例外,都必須付出。 因此,你應該確保編譯器對哪些函式可能會拋出例外有清晰的了解。 如果編譯器能證明某些函式(規範) noexcept 不會有例外,那麼它就能優化所產生的程式碼。

捕捉例外狀況

Windows 執行階段 ABI 層產生的錯誤條件會以 HRESULT 值的形式回傳。 但你不需要在程式碼裡處理 HRESULT。 在消費端為 API 產生的 C++/WinRT 投影程式碼會在 ABI 層偵測錯誤 HRESULT 程式碼,並將該程式碼轉換成 winrt::hresult_error 例外,你可以捕捉並處理。 如果你 處理 HRESULT,請使用 winrt::hresult 型別。

例如,如果使用者在應用程式遍歷該集合時,從圖片庫刪除了圖片,投影就會拋出例外。 而這種情況你必須發現並處理這個例外。 這裡有一個程式碼範例說明這種情況。

#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.h>
#include <winrt/Microsoft.UI.Xaml.Media.Imaging.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Storage;
using namespace Microsoft::UI::Xaml::Media::Imaging;

IAsyncAction MakeThumbnailsAsync()
{
    auto imageFiles{ co_await KnownFolders::PicturesLibrary().GetFilesAsync() };

    for (StorageFile const& imageFile : imageFiles)
    {
        BitmapImage bitmapImage;
        try
        {
            auto thumbnail{ co_await imageFile.GetThumbnailAsync(FileProperties::ThumbnailMode::PicturesView) };
            if (thumbnail) bitmapImage.SetSource(thumbnail);
        }
        catch (winrt::hresult_error const& ex)
        {
            winrt::hresult hr = ex.code(); // HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND).
            winrt::hstring message = ex.message(); // The system cannot find the file specified.
        }
    }
}

在協同程式中呼叫 co_await 修飾的函式時,請使用相同的模式。 另一個從 HRESULT 轉換成例外的例子是,當元件 API 回傳 E_OUTOFMEMORY 時,會拋出 std::bad_alloc

當你只是查看 HRESULT 錯誤碼時,應優先使用 winrt::hresult_error::code。 另一方面, winrt:hresult_error:to_abi 函式會轉換成 COM 錯誤物件,並將狀態推送到 COM 執行緒本地儲存。

拋出例外

有些情況你會決定,如果你對某個函式的呼叫失敗,應用程式將無法恢復(你將無法再依賴它以可預測的方式運作)。 以下程式碼範例使用 winrt::handle 值作為對 CreateEvent 所傳回 HANDLE 的包裝類別。 接著它會將帳柄(從中產生 bool 值)傳給 winrt::check_bool 函式模板。 winrt::check_bool 可搭配 bool 使用,也可搭配任何可轉換為 false(錯誤條件)或 true(成功條件)的值使用。

winrt::handle h{ ::CreateEvent(nullptr, false, false, nullptr) };
winrt::check_bool(bool{ h });
winrt::check_bool(::SetEvent(h.get()));

若傳遞給 winrt::check_bool 的值為假,則執行以下一連串動作。

由於Windows API 會用各種回傳值類型回報執行時錯誤,除了 winrt::check_bool 之外,還有一些其他有用的輔助函式用於檢查值和拋出異常。

你可以用這些輔助函式來處理常見的回傳碼型態,或者對任何錯誤條件回應,呼叫 winrt::throw_last_errorwinrt::throw_hresult

撰寫 API 時拋出例外

所有 Windows 執行階段 應用程式二進位介面邊界(或 ABI 邊界)必須是 noexcept,意即例外絕不能逃逸。 當你撰寫 API 時,應該總是用 C++ noexcept 關鍵字標記 ABI 邊界。 noexcept 在 C++ 中具有特定行為。 如果 C++ 例外遇到 noexcept 邊界,程序會很快以 std::terminate 失敗。 這種行為通常是理想的,因為未處理的例外幾乎總是暗示過程中的未知狀態。

由於例外不能跨越 ABI 邊界,實作中出現的錯誤條件會以 HRESULT 錯誤碼的形式在 ABI 層中回傳。 當你用 C++/WinRT 撰寫 API 時,會產生程式碼讓你將 任何你在 實作中丟棄的例外轉換成 HRESULT。 winrt::to_hresult 函式用於該產生的程式碼中,模式如下。

HRESULT DoWork() noexcept
{
    try
    {
        // Shim through to your C++/WinRT implementation.
        return S_OK;
    }
    catch (...)
    {
        return winrt::to_hresult(); // Convert any exception to an HRESULT.
    }
}

WinRT::to_hresult 處理由 STD::exception 衍生的例外,以及 winrt::hresult_error 及其衍生型別。 在你的實作中,應該偏好 winrt::hresult_error 或導出型別,讓 API 的使用者能獲得豐富的錯誤資訊。 如果在使用標準範本程式庫時發生例外,則支援 std::exception(對應至 E_FAIL)。

使用 noexcept 的可除錯性

如我們上面所提到的,C++ 例外狀況一旦碰到 noexcept 邊界,就會立即以 std::terminate 終止。 這對除錯來說並不理想,因為 std::terminate 往往會遺失大部分甚至全部的錯誤資訊,或所拋出例外的相關脈絡,尤其是在涉及協程時。

因此,本節說明的是這種情況:你已正確加上 noexcept 註解的 ABI 方法,使用 co_await 呼叫非同步的 C++/WinRT 投影程式碼。 我們建議您將 C++/WinRT 投影程式碼的呼叫封裝在 winrt::fire_and_forget 中。 這樣做可讓未處理的例外有適當的位置以暫存例外的形式妥善記錄,從而大幅提升可除錯性。

HRESULT MyWinRTObject::MyABI_Method() noexcept
{
    winrt::com_ptr<Foo> foo{ get_a_foo() };

    [/*no captures*/](winrt::com_ptr<Foo> foo) -> winrt::fire_and_forget
    {
        co_await winrt::resume_background();

        foo->ABICall();

        AnotherMethodWithLotsOfProjectionCalls();
    }(foo);

    return S_OK;
}

winrt::fire_and_forget 內建了 unhandled_exception 方法輔助程式,會呼叫 winrt::terminate,而後者又會呼叫 RoFailFastWithErrorContext。 這確保任何上下文(儲存異常、錯誤代碼、錯誤訊息、堆疊回溯等)都能被保留,無論是即時除錯還是事後備份。 為了方便,你可以將 fire-and-forget 的部分抽出成另一個會回傳 winrt::fire_and_forget 的函式,然後再呼叫該函式。

同步程式碼

在某些情況下,你的 ABI 方法(再說一次,你已使用 noexcept 正確註解標示)只呼叫同步程式碼。 換句話說,它從不使用 co_await,無論是用來呼叫 Windows 執行階段 非同步方法,還是用來在前景與背景執行緒之間切換。 在這種情況下,fire_and_forget 技巧仍然可行,但效率不高。 你可以改成這樣做。

HRESULT abi() noexcept try
{
    // ABI code goes here.
} catch (...) { winrt::terminate(); }

快速失敗

前一節的程式碼仍然很快失敗。 照原文寫法,該程式碼不處理任何例外。 任何未處理的例外都會導致程式終止。

但這種形式更優秀,因為它確保了可除錯性。 在少數情況下,你可能會想要 try/catch,並處理某些例外情況。 但這種情況應該很少見,因為正如本主題所解釋,我們不鼓勵將例外作為預期條件的流量控制機制。

請記得,讓未處理的例外逃離 noexcept 裸露的上下文是個壞主意。 在此條件下,C++ 執行時會 std::終止 程序,導致 C++/WinRT 謹慎記錄的存入異常資訊遺失。

Assertions

對於應用程式中的內部假設,可以使用斷言。 在編譯時驗證時,盡量建議使用 static_assert 。 對於執行時條件,請使用 WINRT_ASSERT 布林運算式。 WINRT_ASSERT 是一個巨集定義,並擴展為 _ASSERTE

WINRT_ASSERT(pos < size());

WINRT_ASSERT 在發行版本建置中會被編譯器移除;在偵錯建置中,則會讓應用程式在偵錯工具中停在包含該判斷提示的那一行程式碼。

你不應該在解構函式中拋出例外。 所以,至少在偵錯組建中,你可以使用 WINRT_VERIFY(搭配布林運算式)和 WINRT_VERIFY_(搭配預期結果與布林運算式),來斷言在解構函式中呼叫函式的結果。

WINRT_VERIFY(::CloseHandle(value));
WINRT_VERIFY_(TRUE, ::CloseHandle(value));

重要 API