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 と一致しません。 ご想像のとおり、その時点では何の良いことが起こりません。
Note
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;
}
このコードと元の例のメイン違いは、このバージョンが明示的に Release を呼び出さない点です。 CComPtr インスタンスがスコープ外になると、デストラクターは基になるポインターで Release を呼び出します。
CComPtr はクラス テンプレートです。 template 引数は COM インターフェイス型です。 内部的には、 CComPtr はその型のポインターを保持します。 CComPtr はoperator-(>) と operator&() をオーバーライドして、クラスが基になるポインターのように動作するようにします。 たとえば、次のコードは 、IFileOpenDialog::Show メソッドを直接呼び出すことと同じです。
hr = pFileOpen->Show(NULL);
CComPtr は CComPtr::CoCreateInstance メソッドも定義します。このメソッドは、既定のパラメーター値を使用して COM CoCreateInstance 関数を呼び出します。 次の例に示すように、必須のパラメーターはクラス識別子のみです。
hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));
CComPtr::CoCreateInstance メソッドは、便宜上、純粋に提供されます。必要に応じて、COM CoCreateInstance 関数を呼び出すことができます。
次へ