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

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

例外のキャッチとスローの回避

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

キャッチできる見込みがある例外をスローしないでください。 また、予想されるエラーに対して例外を使用しないでください。 予期しないランタイム エラーが発生したときにのみ例外をスローし、それ以外はすべてエラー コードまたは結果コードで、エラーの発生源に近いところで直接処理します。 そうすることで、例外がスローされたときに、原因がコード内のバグまたはシステム内の例外的なエラー状態のどちらかであることがわかります。

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

別の例として、StorageFile.GetThumbnailAsync への呼び出しからサムネイル画像を取得し、そのサムネイルを BitmapSource.SetSourceAsyncに渡すことを考えてみましょう。 その一連の呼び出しが原因で nullptrSetSourceAsync に渡されると (画像ファイルは読み取れません。ファイル拡張子を見るとファイルに画像データが含まれているように見えるかもしれませんが、実際には含まれていません)、無効なポインター例外がスローされます。 コード内でそのようなケースが見つかったら、そのケースを例外としてキャッチして処理するのではなく、GetThumbnailAsync から返される nullptr をチェックします。

例外のスローは、エラー コードの使用より遅くなる傾向があります。 致命的なエラーが発生した場合にのみ例外をスローするようにすれば、すべてがうまくいった場合にパフォーマンスのコストを支払わずに済みます。

ただし、より可能性の高いパフォーマンスの影響としては、万が一例外がスローされた場合に適切なデストラクターが確実に呼び出されるようにするためのランタイム オーバーヘッドがあります。 例外が実際にスローされるかどうかに関係なく、この保証に関するコストは発生します。 そのため、どの関数が例外をスローする可能性があるかをコンパイラがきちんと把握しておく必要があります。 特定の関数から例外が発生しないことをコンパイラが証明できれば (noexcept 仕様)、生成されるコードを最適化できます。

例外のキャッチ

Windows ランタイム ABI レイヤーで発生するエラー状態は、HRESULT 値の形式で返されます。 ただし、コードで HRESULT を処理する必要はありません。 使用する側で API 用に生成された C++/WinRT プロジェクション コードにより、ABI レイヤーで HRESULT エラー コードが検出され、そのコードが winrt::hresult_error 例外に変換されます。この例外はキャッチして処理できます。 HRESULTS を処理したい場合は、winrt::hresult 型を使用します。

たとえば、アプリケーションによるそのコレクションの反復処理中に、ユーザーがたまたま画像ライブラリの画像を削除した場合は、プロジェクションにより例外がスローされます。 これは、その例外をキャッチして処理する必要があるケースです。 このケースを示すコード例を次に示します。

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Storage;
using namespace Windows::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 に渡す値が false である場合は、次の一連の処理が実行されます。

  • winrt::check_boolwinrt::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 かどうかをチェックし、null の場合は winrt::throw_last_error を呼び出します。
  • winrt::check_win32。 コードがエラーを表すかどうかをチェックし、エラーを表す場合は winrt::throw_hresult を呼び出します。

これらのヘルパー関数を一般的な戻りコードの種類に使用することも、任意のエラー状態に応答して winrt::throw_last_error または winrt::throw_hresult を呼び出すこともできます。

API を作成するときの例外のスロー

Windows ランタイム アプリケーション バイナリ インターフェイスの境界 (ABI の境界) はすべて noexcept である必要があります。これは、例外がここで絶対にエスケープされないことを意味します。 API を作成するときは、常に、ABI の境界を C++ noexcept キーワードでマークしてください。 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 とその派生型を処理します。 実装では、API のユーザーが詳細なエラー情報を受け取れるように、winrt::hresult_error または派生型を使用することをお勧めします。 標準テンプレート ライブラリの使用によって例外が発生した場合に備えて、std::exception (E_FAIL にマップされる) がサポートされています。

noexcept によるデバッグ容易性

前述のように、C++ の例外で noexcept 境界がヒットすると、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 メソッド ヘルパーがあります。これは winrt::terminate を呼び出し、さらにこれが 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 で、特定の例外を処理することが必要になる場合があります。 しかし、これはまれなケースです。このトピックで説明するように、予想される条件のフロー制御メカニズムとして例外を使用することをお勧めします。

ハンドルされない例外によってネイキッド noexcept コンテキストをエスケープできるようにすることは、適切な方法ではありません。 その条件下では C++ ランタイムは std::terminate でプロセスを呼び出し、これにより、C++/WinRT で慎重に記録された格納済みの例外情報が失われます。

アサーション

アプリケーションの内部の前提として、アサーションが用意されています。 コンパイル時の検証には、可能な限り、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