COM のエラー処理

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

定数数値説明
E_ACCESSDENIED0x80070005アクセスが拒否されました。
E_FAIL0x80004005予測できないエラーです。
E_INVALIDARG0x80070057パラメーターの値が無効です。
E_OUTOFMEMORY0x8007000Eメモリが不足しています。
E_POINTER0x80004003ポインター値に誤って NULL が渡されました。
E_UNEXPECTED0x8000FFFF予期しない状態です。
S_OK0x0成功しました。
S_FALSE0x1成功しました。

 

プレフィックス "E_" を持つ定数はすべてエラー コードです。定数 S_OK と S_FALSE は成功コードです。成功した COM メソッドの 99% は S_OK を返しますが、これを根拠に誤解してしまうことは避けなければなりません。メソッドによっては異なる成功コードを返す場合があるので、常に SUCCEEDED または FAILED マクロを使ってエラーをテストしてください。関数呼び出しの成功をテストする方法として誤っているものと正しいものを以下に示します。

// 誤った方法
HRESULT hr = SomeFunction();
if (hr != S_OK)
{
    printf("Error!\n"); // 不適切。hr は別の成功コードの可能性がある
}

// 正しい方法
HRESULT hr = SomeFunction();
if (FAILED(hr))
{
    printf("Error!\n"); 
}

成功コード S_FALSE について少し詳しく説明します。一部のメソッドでは、簡単に言うと、マイナスの状態ではあるが失敗ではないという意味で S_FALSE を使用している場合があります。また、メソッドが成功したがその影響が発生しないという "何もしない (no-op)" 状態を示す場合にも使用されます。たとえば CoInitializeEx 関数は、同じスレッドから 2 回目に呼び出された場合に S_FALSE を返します。S_OK と S_FALSE を区別する必要がある場合は値を直接テストする必要があります。ただしそれ以外のケースの処理には、以下のコード例のように FAILED または SUCCEEDED を使用してください。

if (hr == S_FALSE)
{
    // 特別なケースを処理する
}
else if (SUCCEEDED(hr))
{
    // 一般的な成功ケースを処理する
}
else
{
    // エラーを処理する
    printf("Error!\n"); 
}

一部の HRESULT 値には、Windows の機能やサブシステムに固有なものがあります。たとえば Direct2D のグラフィックス API が定義するエラー コード D2DERR_UNSUPPORTED_PIXEL_FORMAT は、プログラムが使用するピクセル形式がサポートされていないことを意味しています。多くの MSDN ドキュメントで、メソッドが返す可能性がある特定のエラー コードが一覧として提供されていますが、これらのリストを絶対的なものとして認識しないようにしてください。メソッドが、そうしたドキュメントに記載されていない HRESULT 値を返す可能性は常に存在します。繰り返しになりますが、必ず SUCCEEDEDFAILED のマクロを使うようにしてください。また、特定のエラー コードをテストする場合は、同時に既定のケースも処理してください。

if (hr == D2DERR_UNSUPPORTED_PIXEL_FORMAT)
{
    // ピクセル形式がサポートされていない特定のケースを処理する
}
else if (FAILED(hr))
{
    // その他のエラーを処理する
}

エラー処理のパターン

このセクションでは、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))
            {
                // pItem を使用する (省略) 
                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))
    {
        // pItem を使用する (省略)
    }

    // クリーンアップする
    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;
    }

    // pItem を使用する (省略)

done:
    // クリーンアップする
    SafeRelease(&pItem);
    SafeRelease(&pFileOpen);
    return hr;
}


メリット

  • 全体の制御フローがよりわかりやすくなる。
  • コード内の FAILED チェック後にラベルにジャンプしていなければ、それまでのすべての呼び出しが成功していることになる。
  • コード内の 1 つの場所でリソースが解放される。

デメリット

  • すべての変数を、関数の先頭で宣言および初期化する必要がある。
  • プログラマーによってはコード内での goto の使用が嫌われる (ただしここでの goto の使用方法は非常に構造化されています。現在の関数呼び出しの外側にコードがジャンプすることはありません)。
  • goto ステートメントが初期化子をスキップする。

失敗時にスローする

メソッドが失敗した場合に、ラベルにジャンプするのではなく例外をスローします。例外安全なコードを記述することに慣れている場合、この手法によって、より C++ らしい形式を実現することができます。


#include <comdef.h>  // _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));

        // pItem を使用する (省略)
    }
    catch (_com_error err)
    {
        // エラーを処理する
    }
}


この例ではインターフェイス ポインターの制御に CComPtr クラスを使用している点に注意してください。一般的にコードが例外をスローする場合は、RAII (Resource Acquisition is Initialization: リソースの取得は初期化である) パターンを使用する必要があります。つまり、リソースの正常な解放を保証するデストラクターを持つオブジェクトによって、あらゆるリソースを制御する必要があります。これによって例外がスローされると、デストラクターが必ず開始されます。それ以外の場合、プログラムでメモリ リークが発生する可能性があります。

メリット

  • 例外処理を使用している既存コードとの互換性が確保される。
  • 標準テンプレート ライブラリ (STL: Standard Template Library) などの例外をスローする、C++ ライブラリとの互換性が確保される。

デメリット

  • メモリやファイル ハンドルなどのリソースを制御するための C++ オブジェクトが必要になる。
  • 例外安全なコードを記述する方法に対する深い知識が要求される。

次のトピック

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

 

 

このトピックに関するご意見をお寄せください (英語のみ)。

作成日: 2010 年 10 月 5 日

コミュニティの追加

追加
表示:
© 2015 Microsoft