COM における継承

補足として、COM の中での「継承」の応用について説明します。COM そのものが、特定の OS でしかサポートされていないため、今回説明する内容もウィンドウズの場合の話になります。下のコードは普通の COM プログラムの流れです。

void func( void )
{
    IFoo *pFoo = NULL;

    CoInitialize( NULL );

    CoCreateInstance( 
        CLSID_Foo, 
        NULL, 
        CLSCTX_INPROC, 
        IID_IFoo, 
        ( LPVOID* ) &pFoo );

    if ( pFoo != NULL )
    {
        pFoo->Func();
        pFoo->Release();
        pFoo = NULL;
    }

    CoUninitialize();
}

CoInitialize と CoUninitialize は Win32 の API で、インターフェースを利用する前と後に必ず呼び出す約束になっています。COM 的プログラミングとは、「ライブラリが内部で作成したオブジェクト」へのインターフェース(ポインタではありません)を取得し、そのインターフェースを介してオブジェクトを操作することです。例のコードであれば、IFoo がインターフェースとなって、何かのオブジェクトを操作することになります。「何か」というのは、ライブラリを利用する側には、オブジェクトのことは何もわからないからです。わかっているのは、CLSID_Foo という一意の値をもつクラスがあるということくらいです。具体的な実装については何もわからないし、また、知る必要もありません。

ただし、ここで説明したいのは COM の仕組みではなくて、COM の中で継承がどのように利用されているかということです。そのためには、ライブラリの中身について明らかにしないわけにはいきません。ここでは、ライブラリの中で秘密裏に作成されるオブジェクトのクラスを、仮に CFoo とします。COM の機能を実現する方法は色々ですが、もしライブラリを C++ で書くとすれば、ソースコードは次のようになるでしょう。

class CFoo : public IFoo
{
private:
    ULONG m_ulRefCount;

public:
    CFoo( void );

    HRESULT QueryInterface( REFIID riid, LPVOID *ppv );
    ULONG   AddRef( void );
    ULONG   Release( void );

    HRESULT Func( void );
};

*いずれのメンバ関数も、別のどこかで定義されているものとします。

最初の行に着目してください。CFoo は、なんと IFoo から派生しています。このことは、IFoo のメソッドを呼び出すと、CFoo のメンバ関数が呼び出されることを意味します。なぜなら、IFoo は次のように宣言されるからです。

interface IFoo : IUnknown
{
    HRESULT Func( void );
};

interface という見慣れないキーワードが出てきましたが、これは IFoo がインターフェースであることを示す IDL のキーワードです。IDL とはインターフェイス定義言語のことです( IFoo の宣言は、CFoo の宣言とは異なるソースファイル .idl で行います)。C++ では、インターフェースのメソッドはすべて、純粋仮想関数として扱われます。すなわち、上の IFoo の宣言を C++ 風に書くと、

class IFoo : public IUnknown
{
public:
    virtual HRESULT Func( void ) = 0;
};

となります。そして Func は、IFoo を継承するクラス CFoo でオーバーライドされます。ここで IUnknown とは何かという疑問が生じますが、IUnknown は次のように宣言されます。

interface IUnknown
{
    HRESULT QueryInterface( REFIID riid, void **ppv );
    ULONG   AddRef( void );
    ULONG   Release( void );
};

全てのインターフェースは、最初にこの IUnknown から派生していなければならない決まりになっています。IUnknown には 3 つのメソッドがありますが、やはり全て純粋仮想関数であり、CFoo でオーバーライドされます。

ここで最初のコードに戻り、CoCreateInstance について説明することにします。この関数はレジストリを参照し、引数として渡された CLSID、つまりクラス固有の 128bit の値を、キーの中から探し出します。そこにはクラスを定義しているライブラリのパスが記録されています( CLSID やライブラリのパスは、ライブラリ自身か、セットアッププログラムが記録します)。次に、そのライブラリからエクスポートされている DllGetClassObject という名前の関数を呼び出します。その関数の内部で、クラスオブジェクトと呼ばれるオブジェクトから IClassFactory というインターフェースを取得し、そのメソッド CreateInstance を呼び出します。このメソッドは次のようなことをします。

if ( riid == IID_IFoo )
{
    CFoo *foo = new CFoo();
    if ( foo == NULL ) 
        return E_OUTOFMEMORY;

    hRes = foo->QueryInterface( riid, ppv );
}

new で作ったオブジェクトが、どこで delete されるのかについては心配しないでください。恐らく pFoo->Release() の内部で delete されるはずです。riid は、CoCreateInstance に渡された IID_IFoo であり、ppv は &pFoo です。最後の foo->QueryInterface は、次のような操作を行います。

if ( riid == IID_IFoo ) 
    *ppv = static_cast< IFoo* >( this );

this とは、もちろん new CFoo で作成されたオブジェクトのことです。そのポインタを基底クラスの(厳密にはクラスではありませんが)IFoo* 型にキャストして *ppv に代入します。インターフェースの取得作業は、この後少し処理をして(もちろん、ライブラリの内部で)、ようやく完了することになります。

技術的には、インターフェースを多重継承するクラスを設計することも可能です。

class CGoo : public IFoo, public IGoo
{
    // 略
};

CoCreateInstance に対し、IID_IFoo を渡したときは IFoo を、IID_IGoo のときは IGoo を得ることが可能です。CGoo::QueryInterface は、this を適切にキャストしてインターフェースポインタを与えます。ところで IFoo も IGoo も、同じ IUnknown から派生していることに気が付いたでしょうか。以前、多重継承のところで、この「クラスの重複」が引き起こす問題について説明し、仮想継承を用いて解決する方法を紹介しましたが、ここでは仮想継承していません。それでも問題ない理由は、IUnknown はメソッド(純粋仮想のメンバ関数)しかなく、その定義も CGoo でしかおこなっていないため、pFoo->QueryInterface も pGoo->QueryInterface も、CGoo::QueryInterface を呼び出すことになるからです。多重継承で問題がないわけではないですが、それは省略します。もちろん、IFoo と IGoo を利用する側にしてみれば、単一継承でも多重継承でもどちらでもよく、また、どちらであるかは知り得ないことです。