次の方法で共有


COM でのエラー処理 (Win32 と C++ で作業を始める)

COM では HRESULT 値を使用して、メソッドまたは関数呼び出しの成功または失敗を示します。 さまざまな SDK ヘッダーによって、さまざまな HRESULT 定数が定義されます。 システム全体のコードの一般的なセットは、WinError.h で定義されています。 次の表は、そのようなシステム全体のリターン コードの一部を示しています。

定数 数値 説明
E_ACCESSDENIED 0x80070005 アクセスが拒否されました。
E_FAIL 0x80004005 未定義のエラーが発生しました。
E_INVALIDARG 0x80070057 パラメーターの値が無効です。
E_OUTOFMEMORY 0x8007000E メモリが不足しています。
E_POINTER 0x80004003 ポインター値に対して NULL が正しく渡されませんでした。
E_UNEXPECTED 0x8000FFFF 予期しない状況です。
S_OK 0x0 成功しました。
S_FALSE 0x1 成功しました。

 

プレフィックス "E_" を持つ定数はすべてエラー コードです。 定数 S_OKS_FALSE はどちらも成功コードです。 おそらく、COM メソッドの 99% は成功すると S_OK を返します。しかし、この事実を誤解しないでください。 メソッドが他の成功コードを返す可能性があるため、必ず SUCCEEDED または FAILED マクロを使用してエラーがないかどうかをテストしてください。 次のコード例は、関数呼び出しの成功をテストするための間違った方法と正しい方法を示しています。

// Wrong.
HRESULT hr = SomeFunction();
if (hr != S_OK)
{
    printf("Error!\n"); // Bad. hr might be another success code.
}

// Right.
HRESULT hr = SomeFunction();
if (FAILED(hr))
{
    printf("Error!\n"); 
}

成功コード S_FALSE には説明が必要でしょう。 一部のメソッドでは、大まかに言えば失敗ではない負の状況を意味するために S_FALSE を使用します。 "no-op" と示す場合もあります。メソッドは成功しましたが、効果はありませんでした。 たとえば、CoInitializeEx 関数は、同じスレッドから 2 回目の呼び出しを行うと、S_FALSE を返します。 コード内で S_OKS_FALSE を区別する必要がある場合は、値を直接テストする必要がありますが、次のコード例に示すように、FAILED または SUCCEEDED を使用して残りのケースを処理する必要があります。

if (hr == S_FALSE)
{
    // Handle special case.
}
else if (SUCCEEDED(hr))
{
    // Handle general success case.
}
else
{
    // Handle errors.
    printf("Error!\n"); 
}

一部の HRESULT 値は、Windows の特定の機能またはサブシステムに固有のものです。 たとえば、Direct2D グラフィックス API はエラー コード D2DERR_UNSUPPORTED_PIXEL_FORMAT を定義します。これは、プログラムでサポートされていないピクセル形式が使用されたことを意味します。 多くの場合、MSDN ドキュメントには、メソッドが返す可能性がある特定のエラー コードの一覧が記載されています。 ただし、これらのリストがすべてだと考えてはいけません。 メソッドは、常にドキュメントに記載されていない HRESULT 値を返す場合があります。 繰り返しますが、SUCCEEDEDFAILED のマクロを使用してください。 特定のエラー コードをテストする場合は、既定のケースも含めてください。

if (hr == D2DERR_UNSUPPORTED_PIXEL_FORMAT)
{
    // Handle the specific case of an unsupported pixel format.
}
else if (FAILED(hr))
{
    // Handle other errors.
}

エラー処理のパターン

このセクションでは、COM エラーを構造化された方法で処理するためのいくつかのパターンについて確認します。 それぞれのパターンに長所と短所があります。 ある程度、選択は好みの問題です。 既存のプロジェクトで作業する場合は、特定のスタイルについて説明するコーディング ガイドラインが既にある可能性があります。 どのパターンを採用するかに関係なく、信頼性の高いコードは次の規則に従います。

  • HRESULT を返すすべてのメソッドと関数で、戻り値をチェックしてから続行します。
  • リソースを使用した後に解放します。
  • NULL ポインターなど、無効な、または初期化されていないリソースへのアクセスは試行しません。
  • リソースを解放した後に、使用は試行しません。

これらのルールを念頭に置いて、エラーを処理するための 4 つのパターンを次に示します。

if を入れ子にする

HRESULT を返すすべての呼び出しの後、if ステートメントを使用して成功をテストします。 その後、次のメソッド呼び出しを if ステートメントのスコープ内に配置します。 他の if ステートメントも必要な深さの入れ子にすることができます。 このモジュールの前のコード例では、すべてこのパターンを使用していますが、もう一度示します。

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->Show(NULL);
        if (SUCCEEDED(hr))
        {
            IShellItem *pItem;
            hr = pFileOpen->GetResult(&pItem);
            if (SUCCEEDED(hr))
            {
                // Use pItem (not shown). 
                pItem->Release();
            }
        }
        pFileOpen->Release();
    }
    return hr;
}

長所

  • 変数は、最小限のスコープで宣言できます。 たとえば、pItem は使用されるまで宣言されません。
  • if ステートメント内で、特定のインバリアントが true になります。前のすべての呼び出しが成功し、取得されたすべてのリソースが引き続き有効になります。 前の例では、プログラムが最も内側の if ステートメントに達すると、pItempFileOpen の両方が有効であることがわかります。
  • インターフェイス ポインターやその他のリソースを解放するタイミングが明らかです。 リソースを取得した呼び出しの直後にある if ステートメントの最後にリソースを解放します。

短所

  • 入れ子が深いと読みにくいと感じる人もいます。
  • エラー処理が、他の分岐やループのステートメントと混在します。 これにより、プログラム ロジック全体のフォローが困難になる可能性があります。

if をカスケードする

各メソッド呼び出しの後、if ステートメントを使用して成功をテストします。 メソッドが成功した場合は、if ブロック内に次のメソッド呼び出しを配置します。 ただし、if ステートメントをさらに入れ子にする代わりに、後続の各 SUCCEEDED テストを前の if ブロックの後に配置します。 いずれかのメソッドが失敗した場合、残りの SUCCEEDED テストはすべて、関数の一番下に達するまで失敗します。

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen = NULL;
    IShellItem *pItem = NULL;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));

    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->Show(NULL);
    }
    if (SUCCEEDED(hr))
    {
        hr = pFileOpen->GetResult(&pItem);
    }
    if (SUCCEEDED(hr))
    {
        // Use pItem (not shown).
    }

    // Clean up.
    SafeRelease(&pItem);
    SafeRelease(&pFileOpen);
    return hr;
}

このパターンでは、関数の最後にリソースを解放します。 エラーが発生した場合、関数の終了時に一部のポインターが無効になる可能性があります。 無効なポインターに対して Release を呼び出すと、プログラムがクラッシュします (さらに悪い事態になることもあります)、そのため、NULL へのすべてのポインターを初期化し、それらを解放する前に NULL であるかどうかをチェックする必要があります。 この例では、SafeRelease 関数を使用します。スマート ポインターを使用するのもいいでしょう。

このパターンを使用する場合は、ループ コンストラクトに注意する必要があります。 ループ内で、呼び出しが失敗すると、ループが中断します。

長所

  • このパターンでは、"if を入れ子にする" パターンよりも作成される入れ子が少なくなります。
  • 全体的な制御フローが見やすくなります。
  • リソースが、コード内の 1 つの時点で解放されます。

短所

  • すべての変数を、関数の先頭で宣言および初期化する必要があります。
  • 呼び出しが失敗した場合、その関数は関数を直ちに終了するのではなく、不要なエラー チェックを複数回行います。
  • 障害が発生した後も制御フローは関数を通じて続行されるため、関数の本体全体で無効なリソースにアクセスしないように注意する必要があります。
  • ループ内のエラーに、特殊なケースが必要です。

失敗時にジャンプする

各メソッド呼び出しの後、失敗 (成功ではない) がないかどうかをテストします。 失敗した場合は、関数の下部付近にあるラベルにジャンプします。 ラベルの後、関数を終了する前にリソースを解放します。

HRESULT ShowDialog()
{
    IFileOpenDialog *pFileOpen = NULL;
    IShellItem *pItem = NULL;

    HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFileOpen->Show(NULL);
    if (FAILED(hr))
    {
        goto done;
    }

    hr = pFileOpen->GetResult(&pItem);
    if (FAILED(hr))
    {
        goto done;
    }

    // Use pItem (not shown).

done:
    // Clean up.
    SafeRelease(&pItem);
    SafeRelease(&pFileOpen);
    return hr;
}

長所

  • 全体的な制御フローが見やすくなります。
  • FAILED チェックの後のコードのすべての時点で、ラベルにジャンプしていない場合は、以前のすべての呼び出しが成功していることが保証されます。
  • リソースは、コード内の 1 つの時点で解放されます。

短所

  • すべての変数を、関数の先頭で宣言および初期化する必要があります。
  • コードで goto を使用するのが好きではないプログラマもいます。 (ただし、この goto の使用は高度に構造化されていることに注目してください。コードが現在の関数呼び出しの外部にジャンプすることがありません。)
  • goto ステートメントは初期化子をスキップします。

失敗時にスローする

メソッドが失敗したときに、ラベルにジャンプするのではなく、例外をスローできます。 これにより、例外セーフなコードの記述に慣れている場合は、C++ のより慣用的なスタイルが生成される可能性があります。

#include <comdef.h>  // Declares _com_error

inline void throw_if_fail(HRESULT hr)
{
    if (FAILED(hr))
    {
        throw _com_error(hr);
    }
}

void ShowDialog()
{
    try
    {
        CComPtr<IFileOpenDialog> pFileOpen;
        throw_if_fail(CoCreateInstance(__uuidof(FileOpenDialog), NULL, 
            CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen)));

        throw_if_fail(pFileOpen->Show(NULL));

        CComPtr<IShellItem> pItem;
        throw_if_fail(pFileOpen->GetResult(&pItem));

        // Use pItem (not shown).
    }
    catch (_com_error err)
    {
        // Handle error.
    }
}

この例では、CComPtr クラスを使用してインターフェイス ポインターを管理していることに注目してください。 一般に、コードで例外がスローされる場合は、RAII (リソース取得は初期化に) パターンに従う必要があります。 つまり、すべてのリソースは、そのデストラクターによってリソースが正しく解放されることを保証するオブジェクトによって管理される必要があります。 例外がスローされた場合、デストラクターが必ず呼び出されます。 そうしないと、プログラムがリソースをリークする可能性があります。

長所

  • 例外処理を使用する既存のコードと互換性があります。
  • 標準テンプレート ライブラリ (STL) などの例外をスローする C++ ライブラリと互換性があります。

短所

  • メモリやファイル ハンドルなどのリソースを管理するために、C++ オブジェクトが必要です。
  • 例外セーフなコードの記述方法を十分に理解する必要があります。

次へ

モジュール 3. Windows グラフィックス