COM のコーディングプラクティス
このトピックでは、COM コードをより効果的かつ堅牢にする方法について説明します。
__uuidof演算子
プログラムをビルドすると、次のようなリンカー エラーが発生する可能性があります。
unresolved external symbol "struct _GUID const IID_IDrawable"
このエラーは、GUID 定数が外部リンケージ (extern) で宣言されており、リンカーが定数の定義を見つけられなかったことを意味します。 GUID 定数の値は、通常、スタティック ライブラリ ファイルからエクスポートされます。 Microsoft Visual C++ を使用している場合は、__uuidof 演算子を使用してスタティック ライブラリをリンクする必要がなくなります。 この演算子は、Microsoft 言語拡張機能です。 式から GUID 値を返します。 式には、インターフェイス型名、クラス名、またはインターフェイス ポインターを指定できます。 __uuidofを使用すると、次のように共通項目ダイアログ オブジェクトを作成できます。
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
__uuidof(pFileOpen), reinterpret_cast<void**>(&pFileOpen));
コンパイラはヘッダーから GUID 値を抽出するため、ライブラリのエクスポートは必要ありません。
手記
GUID 値は、ヘッダーで __declspec(uuid( ... ))
を宣言することで、型名に関連付けられます。 詳細については、Visual C++ ドキュメントの __declspec のドキュメントを参照してください。
IID_PPV_ARGS マクロ
CoCreateInstanceと QueryInterface の両方、最終的なパラメーターを void** 型に強制する必要があるのを確認しました。 これにより、型の不一致が発生する可能性があります。 次のコード フラグメントについて考えてみましょう。
// Wrong!
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(
__uuidof(FileOpenDialog),
NULL,
CLSCTX_ALL,
__uuidof(IFileDialogCustomize), // The IID does not match the pointer type!
reinterpret_cast<void**>(&pFileOpen) // Coerce to void**.
);
このコードは IFileDialogCustomizeインターフェイスを要求しますが、IFileOpenDialog ポインターを渡します。 reinterpret_cast 式は C++ 型システムを回避するため、コンパイラはこのエラーをキャッチしません。 最良のケースでは、オブジェクトが要求されたインターフェイスを実装していない場合、呼び出しは単に失敗します。 最悪の場合、関数は成功し、ポインターが一致しません。 つまり、ポインター型がメモリ内の実際の vtable と一致しません。 ご想像のとおり、その時点で何も良いことは起こりません。
手記
vtable (仮想メソッド テーブル) は、関数ポインターのテーブルです。 vtable は、実行時に COM がメソッド呼び出しを実装にバインドする方法です。 偶然ではないが、vtable はほとんどの C++ コンパイラが仮想メソッドを実装する方法です。
IID_PPV_ARGS マクロは、このクラスのエラーを回避するのに役立ちます。 このマクロを使用するには、次のコードを置き換えます。
__uuidof(IFileDialogCustomize), reinterpret_cast<void**>(&pFileOpen)
こっちと:
IID_PPV_ARGS(&pFileOpen)
マクロはインターフェイス識別子の __uuidof(IFileOpenDialog)
を自動的に挿入するため、ポインター型と一致することが保証されます。 変更された (正しい) コードを次に示します。
// Right.
IFileOpenDialog *pFileOpen;
hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL, CLSCTX_ALL,
IID_PPV_ARGS(&pFileOpen));
QueryInterfaceで同じマクロを使用できます。
IFileDialogCustomize *pCustom;
hr = pFileOpen->QueryInterface(IID_PPV_ARGS(&pCustom));
SafeRelease パターン
参照カウントは、基本的に簡単ですが、面倒なので間違いを起こしやすいプログラミングの 1 つです。 一般的なエラーは次のとおりです。
- インターフェイス ポインターの使用が完了したときに、インターフェイス ポインターを解放できない。 このクラスのバグにより、オブジェクトが破棄されないため、プログラムはメモリやその他のリソースをリークします。
- 無効なポインター Release を呼び出します。 たとえば、このエラーは、オブジェクトが作成されなかった場合に発生する可能性があります。 このカテゴリのバグにより、プログラムがクラッシュする可能性があります。
- Releaseが呼び出された後インターフェイス ポインターを逆参照します。 このバグにより、プログラムがクラッシュする可能性があります。 さらに悪いことに、後でランダムにプログラムがクラッシュし、元のエラーを追跡するのが難しくなります。
これらのバグを回避する 1 つの方法は、ポインターを安全に解放する関数 Release を呼び出すことです。 次のコードは、これを行う関数を示しています。
template <class T> void SafeRelease(T **ppT)
{
if (*ppT)
{
(*ppT)->Release();
*ppT = NULL;
}
}
この関数は、COM インターフェイス ポインターをパラメーターとして受け取り、次の処理を行います。
- ポインターが NULL かどうかを確認します。
- ポインター NULL がされていない場合は、Release を呼び出します。
- ポインターを null 設定します。
SafeRelease
の使用方法の例を次に示します。
void UseSafeRelease()
{
IFileOpenDialog *pFileOpen = NULL;
HRESULT hr = CoCreateInstance(__uuidof(FileOpenDialog), NULL,
CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pFileOpen));
if (SUCCEEDED(hr))
{
// Use the object.
}
SafeRelease(&pFileOpen);
}
CoCreateInstance成功すると、SafeRelease
の呼び出しによってポインターが解放されます。 CoCreateInstance 失敗した場合、pFileOpen は NULL 残ります。
SafeRelease
関数は、これをチェックし、Releaseへの呼び出しをスキップします。
次に示すように、同じポインターで SafeRelease
を複数回呼び出しても安全です。
// Redundant, but OK.
SafeRelease(&pFileOpen);
SafeRelease(&pFileOpen);
COM スマート ポインター
SafeRelease
関数は便利ですが、次の 2 つを覚えておく必要があります。
- すべてのインターフェイス ポインターを NULL 初期化します。
- 各ポインターがスコープ外になる前に
SafeRelease
を呼び出します。
C++ プログラマは、これらのことを覚えておく必要はない、と考えているでしょう。 結局のところ、C++にはコンストラクターとデストラクターがある理由です。 基になるインターフェイス ポインターをラップし、ポインターを自動的に初期化して解放するクラスがあると便利です。 言い換えると、次のようなものが必要です。
// Warning: This example is not complete.
template <class T>
class SmartPointer
{
T* ptr;
public:
SmartPointer(T *p) : ptr(p) { }
~SmartPointer()
{
if (ptr) { ptr->Release(); }
}
};
ここに示すクラス定義は不完全であり、示されているように使用できません。 少なくとも、コピー コンストラクター、代入演算子、および基になる COM ポインターにアクセスする方法を定義する必要があります。 さいわい、Microsoft Visual Studio にはアクティブ テンプレート ライブラリ (ATL) の一部としてスマート ポインター クラスが既に用意されているため、この作業を行う必要はありません。
ATL スマート ポインター クラスの名前は CComPtr です。 (ここでは説明しない CComQIPtr クラスもあります)。CComPtr を使用するように書き直した例 開くダイアログ ボックス次に示します。
#include <windows.h>
#include <shobjidl.h>
#include <atlbase.h> // Contains the declaration of CComPtr.
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow)
{
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED |
COINIT_DISABLE_OLE1DDE);
if (SUCCEEDED(hr))
{
CComPtr<IFileOpenDialog> pFileOpen;
// Create the FileOpenDialog object.
hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
if (SUCCEEDED(hr))
{
// Show the Open dialog box.
hr = pFileOpen->Show(NULL);
// Get the file name from the dialog box.
if (SUCCEEDED(hr))
{
CComPtr<IShellItem> pItem;
hr = pFileOpen->GetResult(&pItem);
if (SUCCEEDED(hr))
{
PWSTR pszFilePath;
hr = pItem->GetDisplayName(SIGDN_FILESYSPATH, &pszFilePath);
// Display the file name to the user.
if (SUCCEEDED(hr))
{
MessageBox(NULL, pszFilePath, L"File Path", MB_OK);
CoTaskMemFree(pszFilePath);
}
}
// pItem goes out of scope.
}
// pFileOpen goes out of scope.
}
CoUninitialize();
}
return 0;
}
このコードと元の例の主な違いは、このバージョンではリリース明示的に呼び出されないことです。 CComPtr インスタンスがスコープ外になると、デストラクターは基になるポインター Release を呼び出します。
CComPtr はクラス テンプレートです。 テンプレート引数は COM インターフェイス型です。 内部的には、CComPtr はその型のポインターを保持します。 CComPtr は、演算子>() と 演算子&() をオーバーライドして、クラスが基になるポインターのように動作するようにします。 たとえば、次のコードは、IFileOpenDialog::Show メソッドを直接呼び出すことと同じです。
hr = pFileOpen->Show(NULL);
CComPtr では、CComPtr::CoCreateInstance メソッドも定義されます。このメソッドは、いくつかの既定のパラメーター値を持つ COM CoCreateInstance 関数を呼び出します。 次の例に示すように、必要なパラメーターはクラス識別子のみです。
hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
CComPtr::CoCreateInstance メソッドは、便宜上提供されます。必要に応じて、COM CoCreateInstance 関数を呼び出すことができます。
次に
COM でのエラー処理の