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_OK定数とS_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" を示すこともできます。メソッドは成功しましたが、効果はありませんでした。 たとえば、同じスレッドから 2 回目に呼び出した場合、 CoInitializeEx 関数は 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 値を常に返すことができます。 ここでも、 SUCCEEDED マクロと FAILED マクロを 使用します。 特定のエラー コードをテストする場合は、既定のケースも含めます。

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 つのパターンを次に示します。

入れ子になった ifs

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 ステートメントの最後にリソースを解放します。

短所

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

カスケード ifs

各メソッド呼び出しの後に、 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 使用します。スマート ポインターも適切な選択肢です。

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

長所

  • このパターンでは、"入れ子になった ifs" パターンよりも少ない入れ子が作成されます。
  • 全体的な制御フローが見やすくなります。
  • リソースは、コード内の 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 グラフィックス