C++/WinRT でのエラー処理

このトピックでは、 C++/WinRT を使用してプログラミングするときにエラーを処理する方法について説明します。 一般的な情報と背景については、「 エラーと例外処理 (Modern C++)」を参照してください。

例外のキャッチとスローを避ける

例外セーフコードは引き続き記述することをお勧めしますが、可能な限り例外をキャッチしてスローしないようにすることをお勧めします。 例外のハンドラーがない場合、Windowsは自動的にエラー レポート (クラッシュのミニダンプを含む) を生成します。これにより、問題の場所を追跡するのに役立ちます。

キャッチすると予想される例外をスローしないでください。 また、予期されるエラーには例外を使用しないでください。 予期しないランタイム エラーが発生した場合にのみ例外をスローし、エラー/結果コードを使用して他のすべてを直接処理し、エラーの原因に近い状態で処理します。 これにより、例外 スローされると、原因がコードのバグか、システムの例外的なエラー状態のいずれかであることがわかります。

Windows レジストリにアクセスするシナリオについて考えてみましょう。 アプリがレジストリからの値の読み取りに失敗した場合は、それが予想されるため、適切に処理する必要があります。 例外をスローするのではなく、値を読み取れなかったこと、また場合によってはその理由を示す bool または enum の値を返してください。 一方、レジストリに値を 書き込 めなかった場合は、アプリケーションで適切に処理できるよりも大きな問題があることを示している可能性があります。 このような場合は、アプリケーションを続行したくないので、エラー レポートを発生させる例外は、アプリケーションが損害を引き起こさないようにするための最速の方法です。

別の例として、 StorageFile.GetThumbnailAsync の呼び出しからサムネイル画像を取得し、そのサムネイルを BitmapSource.SetSourceAsync に渡すことを検討してください。 その一連の呼び出しでnullptrを SetSourceAsync に渡す場合 (イメージ ファイルを読み取ることはできません。おそらく、そのファイル拡張子を使用すると、イメージ データが含まれているように見えますが、実際には含まれません)、無効なポインター例外がスローされます。 例外としてケースをキャッチして処理するのではなく、コードでそのようなケースを検出した場合は、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_awaited 関数を呼び出すときは、コルーチンでも同じパターンを使用します。 この HRESULT から例外への変換のもう 1 つの例は、コンポーネント API がE_OUTOFMEMORYを返したときに std::bad_alloc がスローされることです。

HRESULT コードを確認するだけなら、winrt::hresult_error::code を使用してください。 一方 、winrt::hresult_error::to_abi 関数は COM エラー オブジェクトに変換され、COM スレッド ローカル ストレージに状態をプッシュします。

例外のスロー

特定の関数の呼び出しが失敗した場合、アプリケーションは復旧できないと判断する場合があります (予測どおりに関数に依存することはできなくなります)。 次のコード例では、CreateEvent から返される HANDLE のラッパーとして winrt::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 に渡す値が false の場合、次の一連のアクションが実行されます。

  • winrt::check_bool はwinrt::throw_last_error 関数を呼び出します。
  • winrt::throw_last_errorGetLastError を呼び出して呼び出し元のスレッドの最後のエラー コード値を取得し、 winrt::throw_hresult 関数を呼び出します。
  • winrt::throw_hresult は、そのエラー コードを表す winrt::hresult_error オブジェクト (または標準オブジェクト) を使用して例外をスローします。

Windows API はさまざまな戻り値型を使用して実行時エラーを報告するため、winrt::check_boolに加えて、値のチェックと例外のスローに役立つヘルパー関数がいくつかあります。

  • winrt::check_hresult。 HRESULT コードがエラーを表しているかどうかを確認し、存在する場合は winrt::throw_hresult を呼び出します。
  • winrt::check_nt。 コードがエラーを表しているかどうかを確認し、存在する場合は winrt::throw_hresult を呼び出します。
  • winrt::check_pointer。 ポインターが null であるかどうかを確認し、存在する場合は winrt::throw_last_error を呼び出します。
  • winrt::check_win32。 コードがエラーを表しているかどうかを確認し、存在する場合は winrt::throw_hresult を呼び出します。

これらのヘルパー関数は、一般的なリターン コード型に使用することも、エラー状態に応答して winrt::throw_last_error または winrt::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 を使用したデバッグ可能性

前述のように、 noexcept 境界に達した C++ 例外は std::terminate で高速に失敗します。 std::terminate は多くの場合、エラーまたはスローされた例外コンテキストの多くまたはすべてを失うため、デバッグには適していません。特にコルーチンが関係している場合です。

そのため、このセクションでは、ABI メソッド ( noexcept で適切に注釈を付けた) が 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 を呼び出す組み込みの メソッド ヘルパーがあり、これにより RoFailFastWithErrorContext が呼び出されます。 これにより、ライブ デバッグまたは事後ダンプのために、コンテキスト (格納された例外、エラー コード、エラー メッセージ、スタック バックトレースなど) が保持されます。 便宜上、fire-and-forget 部分を winrt::fire_and_forget を返す別の関数に分解し、それを呼び出すことができます。

同期コード

場合によっては、ABI メソッド (もう一度、 noexceptで適切に注釈を付けた) は同期コードのみを呼び出します。 つまり、非同期Windows ランタイム メソッドを呼び出したり、フォアグラウンド スレッドとバックグラウンド スレッドを切り替えたりするために、co_awaitを使用することはありません。 その場合でも、fire-and-forget 手法は依然として動作しますが、効率的ではありません。 代わりに、次のようなことができます。

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

早く失敗する

前のセクションのコードは引き続き高速で失敗します。 記述されているように、そのコードは例外を処理しません。 ハンドルされない例外が発生すると、プログラムが終了します。

ただし、その形式は、デバッグが確実になるため、優れています。 まれに、特定の例外を try/catchして処理することが必要になる場合があります。 ただし、このトピックで説明するように、予期される条件のフロー制御メカニズムとして例外を使用しないことをお勧めします。

naked noexcept コンテキストの外に未処理の例外を出さないように注意してください。 その条件の下で、C++ ランタイムは std::terminate プロセスを行い、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