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 値を抽出するため、ライブラリのエクスポートは必要ありません。

Note

GUID 値は、ヘッダーで宣言 __declspec(uuid( ... )) することで型名に関連付けられます。 詳細については、Visual C++ ドキュメントの __declspec のドキュメントを参照してください。

 

IID_PPV_ARGS マクロ

CoCreateInstanceQueryInterface の両方で、最終的なパラメーターを 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 インターフェイス ポインターをパラメーターとして受け取り、次の処理を行います。

  1. ポインターが NULL かどうかを確認 します
  2. ポインターが NULL でない場合は Release を呼び出します。
  3. ポインターを 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 が失敗した場合、pFileOpenNULL のままです。 この関数はこの 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 はクラス テンプレートです。 テンプレート引数は COM インターフェイス型です。 内部的には、 CComPtr はその型のポインターを保持します。 CComPtroperator->()operator&() をオーバーライドして、クラスが基になるポインターのように動作するようにします。 たとえば、次のコードは 、IFileOpenDialog::Show メソッドを直接呼び出すことと同じです。

hr = pFileOpen->Show(NULL);

CComPtrCComPtr::CoCreateInstance メソッドも定義します。このメソッドは、いくつかの既定のパラメーター値で COM CoCreateInstance 関数を呼び出します。 次の例に示すように、必要なパラメーターはクラス識別子のみです。

hr = pFileOpen.CoCreateInstance(__uuidof(FileOpenDialog));

CComPtr::CoCreateInstance メソッドは、便宜上純粋に提供されます。必要に応じて、COM CoCreateInstance 関数を呼び出すことができます。

次へ

COM でのエラー処理