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" を示すこともできます。メソッドは成功しましたが、効果はありませんでした。 たとえば、CoInitializeEx 関数は、同じスレッドから 2 回目に呼び出した場合に S_FALSE を返します。 コードで S_OK と S_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 を定義します。これは、プログラムがサポートされていないピクセル形式を使用したことを意味します。 多くの場合、Windows ドキュメントには、メソッドが返す可能性がある特定のエラー コードの一覧が記載されています。 ただし、これらのリストが明確であるとは考えるべきではありません。 メソッドは、ドキュメントに記載されていない 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 つのパターンを次に示します。
Nested ifs
HRESULT を返すすべての呼び出しの後に、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 ステートメントに達すると、pItem と pFileOpen の両方が有効であることがわかっています。
- インターフェイス ポインターやその他のリソースを解放するタイミングは明確です。 リソースを取得した呼び出し直後の if ステートメントの最後にリソースを解放します。
短所
- 深い入れ子が読みにくい人もいます。
- エラー処理は、他の分岐ステートメントやループ ステートメントと混在しています。 これにより、全体的なプログラム ロジックを理解するのが困難になる可能性があります。
Cascading 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
関数を使用します。スマート ポインターも適切な選択肢です。
このパターンを使用する場合は、ループ構造に注意する必要があります。 ループ内で、呼び出しが失敗した場合はループから中断します。
長所
- このパターンでは、"nested ifs" パターンよりも入れ子が少なくなります。
- 全体的な制御フローが見やすくなります。
- リソースは、コード内のある時点で解放されます。
短所
- すべての変数は、関数の先頭で宣言および初期化する必要があります。
- 呼び出しが失敗した場合、関数は直ちに終了するのではなく、不要なエラー チェックを複数行います。
- 障害が発生した後も制御フローは関数を通じて継続されるため、関数の本体全体で無効なリソースにアクセスしないように注意する必要があります。
- ループ内のエラーには特別なケースが必要です。
Jump on Fail
各メソッド呼び出しの後、失敗 (成功ではない) をテストします。 失敗した場合は、関数の下部付近にあるラベルにジャンプします。 ラベルの後、関数を終了する前にリソースを解放します。
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 チェックの後、コード内のすべての時点で、ラベルにジャンプしていない場合は、以前のすべての呼び出しが成功することが保証されます。
- リソースは、コード内のある時点で解放されます。
短所
- すべての変数は、関数の先頭で宣言および初期化する必要があります。
- 一部のプログラマは、コードで goto を使用することを好まない場合があります。 (ただし、この goto の使用は高度に構造化されていることに注意してください。コードは現在の関数呼び出し外にジャンプすることはありません)。
- goto ステートメントは初期化子をスキップします。
Throw on Fail
ラベルにジャンプするのではなく、メソッドが失敗したときに例外をスローできます。 これにより、例外安全コードの記述に慣れている場合は、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 クラスを使用して COM インターフェイス ポインターを管理することに注意してください。 一般に、コードが例外をスローする場合は、RAII (リソース取得は初期化) パターンに従う必要があります。 つまり、すべてのリソースは、リソースが正しく解放されることを保証するデストラクターを持つオブジェクトによって管理する必要があります。 例外がスローされた場合、デストラクターは呼び出されていることが保証されます。 そうしないと、プログラムがリソースをリークする可能性があります。
長所
- 例外処理を使用する既存のコードと互換性があります。
- 標準テンプレート ライブラリ (STL) などの例外をスローする C++ ライブラリと互換性があります。
短所
- メモリやファイル ハンドルなどのリソースを管理するには、C++ オブジェクトが必要です。
- 例外安全コードを記述する方法を十分に理解する必要があります。
次へ