継承とインターフェイス
C++ モデルと Object Pascal モデル への移動
C++ とは異なり、Object Pascal 言語は多重継承をサポートしません。 RTL を上位クラスとして作成されるクラスはすべて、この制限を継承します。 つまり、Delphi 形式の C++ クラスには、複数の基底クラスを使用できません。RTL クラスを直接の上位クラスとしない場合も、同様です。
目次
多重継承の代わりにインターフェイスを使用する
多くの場合、C++ では多重継承を用いることになる状況で、Object Pascal のコードでは代わりにインターフェイスを利用します。Object Pascal のインターフェイスの概念に、直接マッピングされる C++ の構造はありません。Object Pascal のインターフェイスは、実装のないクラスのように動作します。つまり、インターフェイスとは、すべてのメソッドが純粋仮想で、データ メンバをもたないクラスのようなものです。Object Pascal のクラスは親クラスを 1 つしか持ちませんが、インターフェイスはいくつでもサポートできます。Object Pascal のコードでは、クラス インスタンスをどのインターフェイス型の変数にも代入できます。同様に、クラス インスタンスはどの上位クラス型の変数にも代入できます。これにより、共通の上位クラスを持たなくても、同じインターフェイスを共有するクラスの多態的(ポリモーフィック)な動作が実現できます。
C++Builder のコンパイラは、純粋仮想メソッドのみを持ち、データ メンバを持たないクラスを、Object Pascal のインターフェイスに相当すると認識します。 そのため、Delphi 形式のクラスを作成するときは多重継承を使用できますが、RTL(つまり Delphi 形式のクラス)そのものを除くすべての基底クラスがデータ メンバを持たず、純粋仮想メソッドのみを持つ場合だけです。
メモ: インターフェイス クラスは Delphi 形式のクラスである必要はありません。唯一の要件は、データ メンバを持たず、純粋仮想メソッドのみを持つことです。
インターフェイス クラスを宣言する
インターフェイスを表すクラスは、他の C++ のクラスと同じように宣言できます。ただし、特定の規約をいくつか使用すると、そのクラスをインターフェイスとして動作させることを、明示できます。これらの規約を次に示します。
- class キーワードの代わりに、
__interface
を使用してインターフェイスを宣言します。__interface は、class キーワードをマップするマクロです。これは必須ではありませんが、そのクラスはインターフェイスとして動作することが意図されている、と明示します。
- インターフェイスの名前は通常、'
I
' で始めます(IComponentEditor、IDesigner など)。この規約に従えば、クラスがインターフェイスとして動作するかどうか確認するのに、そのクラスの宣言を見直す必要がありません。
- 通常、インターフェイスには GUID を関連付けます。これは絶対要件ではありませんが、インターフェイスをサポートするコードの多くは、GUID を検出できることを前提としています。uuid 引数の __declspec 修飾子を用いると、インターフェイスと GUID を関連付けることができます。たとえば、INTERFACE_UUID マクロは、同じことをマップしています。
これらの規約に従ったインターフェイス宣言を次に示します。
__interface INTERFACE_UUID("{C527B88F-3F8E-1134-80e0-01A04F57B270}") IHelloWorld : public IInterface
{
public:
virtual void __stdcall SayHelloWorld( void ) = 0 ;
};
通常、インターフェイス クラスを宣言すると、C++Builder のコードでは、対応する DelphiInterface クラスも宣言されます。DelphiInterface クラスは、インターフェイスをより使いやすくします。
typedef System::DelphiInterface<IHelloWorld> _di_IHelloWorld;
IUnknown と IInterface
Object Pascal のすべてのインターフェイスは、共通の祖先 IInterface から派生します。Delphi 形式のクラスが C++ のインターフェイス クラスを追加の基底クラスとして使用できるという意味では、C++ のインターフェイス クラスが IInterface を基底クラスとして使用する必要はありません。ただし、インターフェイスを扱う RTL のコードは、IInterface メソッドがあることを前提としています。
COM プログラミングでは、すべてのインターフェイスが IUnknown から派生します。RTL の COM サポートは、IInterface を直接マッピングする IUnknown の定義に基づいています。つまり、Object Pascal では、IUnknown と IInterface は同じものです。
ただし、Object Pascal の IUnknown 定義は、C++Builder で使用される IUnknown の定義に相当しません。ファイル unknwn.h では、IUnknown が定義され、次の 3 つのメソッドを含みます。
virtual HRESULT STDCALL QueryInterface( const GUID &guid, void **ppv ) = 0;
virtual ULONG STDCALL AddRef() = 0;
virtual ULONG STDCALL Release() = 0;
これは、COM 仕様の一部として Microsoft が定義する、IUnknown の定義に相当します。
Object Pascal の場合とは異なり、C++Builder の IInterface 定義と IUnknown 定義は、同一のものではありません。代わりに、IInterface は、追加メソッド Supprts を含む IUnknown から派生します。
template <typename T>
HRESULT __stdcall Supports( DelphiInterface<T>& smartIntf )
{
return QueryInterface( __uuidof (T),
reinterpret_cast<void **>( static_cast <T**>(&smartIntf)));
}
Supports を使用すると、IInterface を実装するオブジェクトから、サポートされる他のインターフェイスの DelphiInterface を取得できます。そのため、たとえば、IMyFirstInterface というインターフェイスが DelphiInterface を持ち、実装するオブジェクトも IMySecondInterface(このインターフェイスの DelphiInterface 型は _di_IMySecondInterface)を実装している場合、次のように、2 つ目のインターフェイスの DelphiInterface を取得できます。
_di_IMySecondInterface MySecondIntf;
MyFirstIntf->Supports( MySecondIntf );
RTL は、Object Pascal で IUnknown のマッピングを使用します。このマッピングにより、IUnknown メソッドで使用される型が Object Pascal の型に変換され、AddRef メソッドと Release メソッドの名前が _AddRef と _Release に変更されます。これは、これらのメソッドが直接呼び出されるべきではないと示すためです。(Object Pascal では、コンパイラが自動的に、IUnknown メソッドに必要な呼び出しを生成します。) RTL オブジェクトがサポートする IUnknown(または IInterface)メソッドが、C++ にマッピングされ直すと、次のメソッド シグネチャになります。
virtual HRESULT __stdcall QueryInterface( const GUID &IID, void *Obj );
int __stdcall _AddRef( void );
int __stdcall _Release( void );
つまり、Object Pascal で IUnknown や IInterface をサポートする RTL オブジェクトは、C++Builder の IUnknown や IInterface のバージョンはサポートしません。
IUnknown をサポートするクラスを作成する
RTL の多くのクラスには、インターフェイス サポートが組み込まれています。事実、すべての Delphi 形式クラスの基底クラスである TObject には、オブジェクト インスタンスがサポートしている任意のインターフェイスの DelphiInterface クラスを取得できる GetInterface メソッドがあります。TComponent と TInterfacedObject はどちらも、IInterface(IUnknown)のメソッドを実装しています。このため、作成する TComponent や TInterfacedObject の下位クラスでは、Object Pascal のすべてのインターフェイスに共通するメソッドのサポートを自動的に継承します。Object Pascal では、これらのクラスの下位クラスを作成する際には、新しいインターフェイスで導入されるメソッドのみ実装し、継承した IInterface のメソッドについてはデフォルト実装を利用することにより、新しいインターフェイスをサポートすることができます。 残念ながら、IUnknown のメソッドと IInterface のメソッドのシグネチャが、C++Builder と RTL のクラスで使用されているバージョンとで異なるため、作成する Delphi 形式のクラスがたとえ TComponent や TInterfacedObject から(直接または間接に)派生したとしても、それらのクラスには IInterface や IUnknown のサポートは自動的には組み込まれません。つまり、IUnknown や IInterface から派生したインターフェイスを C++ で定義してサポートするには、やはり、IUnknown のメソッドの実装を追加する必要があります。
TComponent や TInterfacedObject から派生したクラスで IUnknown のメソッドを実装するには、関数のシグネチャは異なりますが、IUnknown の組み込みサポートを利用するのが最も簡単です。継承した Object Pascal ベースのバージョンに委譲するように、IUnknown のメソッドの C++ バージョンを実装するだけです。以下はその例です。
virtual HRESULT __stdcall QueryInterface(const GUID &IID, void **Obj)
{
return TInterfacedObject::QueryInterface(IID, (void *)Obj);
virtual ULONG __stdcall AddRef()
{
return TInterfacedObject::_AddRef();
}
// ...
virtual ULONG __stdcall Release()
{
return TInterfacedObject::_Release();
}
上記の 3 つのメソッド実装を TInterfacedObject の下位クラスに追加することにより、そのクラスは IUnknown または IInterface を完全にサポートします。
IUnknown のメソッドが自動的に実装されるようにする場合は、systobj.h に定義されているマクロ INTFOBJECT_IMPL_IUNKNOWN(BASE)
を次のように使用することができます。
class <class name>: public <intf name> {
INTFOBJECT_IMPL_IUNKNOWN(<base>);
public:
// no need to implement QueryInterface, AddRef and Release
};
IUnknown のメソッドの実装を避けるには、TInterfacedObject ではなく TCppInterfacedObject を使用することができます。このクラスには QueryInterface、AddRef、Release が既に実装されています。
インターフェイス化したクラスと存続時間管理
インターフェイス クラスの IUnknown メソッドは、そのインターフェイスを実装するオブジェクトを割り当てたり解放する方法に影響を与えます。COM オブジェクトで IUnknown を実装した場合、そのオブジェクトは IUnknown メソッドを使用して、使用されている、そのインターフェイスへの参照の数を追跡します。参照カウントが 0 になったときに、オブジェクトは自動的に自身を解放します。TInterfacedObject は IUnknown メソッドを実装して、これと同様の存続時間管理を行います。
ただし、このような IUnknown メソッドの解釈は、必ずしも必要ではありません。たとえば、TComponent が提供する IUnknown メソッドのデフォルトの実装は、インターフェイスの参照カウントを無視するため、このコンポーネントは参照カウントが 0 になっても自身を解放しません。TComponent は、自身の Owner プロパティで指定するオブジェクトに、解放されるからです。
これら 2 つのモデルのハイブリッドを使用するコンポーネントもあります。それらは、Owner プロパティが NULL の場合、存続時間管理にインターフェイスの参照カウントを使用して、参照カウントが 0 になったときに自身を解放します。所有者がある場合は、参照カウントを無視して、所有者が解放できるようにします。このようなハイブリッド オブジェクトは、アプリケーションがそのオブジェクトを作成しても、そこからインターフェイスを 1 つも取得しなかった場合は、自動的に開放されないことに注意してください。これは、存続時間管理に参照カウントを利用する他のオブジェクトも同様です。