テクニカル ノート 16: MFC における C++ の多重継承
更新 : 2007 年 11 月
ここでは、Microsoft Foundation Class における多重継承 (MI: Multiple Inheritance) の使い方について説明します。MFC では、MI の使用は必須ではありません。MI はどの MFC クラスでも使用されていませんし、これを使わなくてもクラス ライブラリを作成できます。
ここでは、MI によって MFC の一般的な表現形式の使い方がどのように変わるかについて説明し、MI に伴ういくつかの制限事項について取り上げます。そのような制限事項の中には、C++ の一般的な制限事項もあります。また、MFC アーキテクチャに由来する制限事項もあります。
このテクニカル ノートの最後には、MI を使用した完全な MFC アプリケーションも記載しています。
CRuntimeClass
MFC の永続性および動的オブジェクト作成機構では CRuntimeClass データ構造体を使ってクラスを一意に識別します。MFC では、これらの構造体をアプリケーション内の個別の動的クラスやシリアル化可能クラスに 1 つずつ関連付けます。これらの構造体は、アプリケーションの起動時に AFX_CLASSINIT 型の特別な静的オブジェクトを使用して初期化されます。
CRuntimeClass の現在の実装では、実行時の型情報として MI をサポートしていません。とはいえ、MFC アプリケーションで MI を使用できない、という意味ではありません。ただし、複数の基本クラスがあるオブジェクトを操作するときには、いくつかの注意点があります。
CObject::IsKindOf メソッドは、オブジェクトに基本クラスが複数あると、オブジェクトの型を正しく判定できません。したがって、CObject を仮想基本クラスとして使えません。また、スコープ修飾子を付けずに CObject のメンバ関数の CObject::Serialize や CObject::operator new などを呼び出すと、C++ で適切な関数呼び出しを特定できなくなります。MFC で MI を使用するプログラムの場合は、基本クラス リストの中で、CObject 基本クラスが含まれているクラスを一番左に記述する必要があります。
代替手段として、dynamic_cast 演算子を使用できます。MI を使用したオブジェクトをいずれかの基本クラスにキャストすると、コンパイラは必ず、その渡された基本クラスの関数を使用することになります。詳細については、「dynamic_cast Operator」を参照してください。
CObject - すべてのクラスのルート
重要なクラスはどれも CObject クラスから直接または間接的に派生しています。CObject にはメンバ データはありませんが、既定の機能がいくつかあります。MI を使用する場合は、複数の CObject 派生クラスから継承するのが一般的です。クラスが CFrameWnd と CObList から継承する例を以下に示します。
class CListWnd : public CFrameWnd, public CObList
{
...
};
CListWnd myListWnd;
この場合は、CObject を 2 回組み込んでいます。CObject のメソッドや演算子の参照のあいまいさをなくす方法が必要だからです。ここであいまいさをなくす必要がある 2 つの演算子は、operator new と operator delete です。次に示すのは、コンパイル時にエラーになる例です。
myListWnd.Dump(afxDump);
// compile time error, CFrameWnd::Dump or CObList::Dump ?
CObject のメソッドの再実装
CObject から派生した複数のクラスを基本クラスとして新しいクラスを作成するときは、CObject のメソッドのうち、他のユーザーが使用するメソッドを実装し直します。演算子 new と delete は必ず実装し直してください。Dump も実装し直すことをお勧めします。new 演算子、delete 演算子、Dump メソッドを実装し直す例を次に示します。
class CListWnd : public CFrameWnd, public CObList
{
public:
void* operator new(size_t nSize)
{ return CFrameWnd::operator new(nSize); }
void operator delete(void* p)
{ CFrameWnd::operator delete(p); }
void Dump(CDumpContent& dc)
{ CFrameWnd::Dump(dc);
CObList::Dump(dc); }
...
};
CObject の仮想継承
CObject の仮想継承によって関数のあいまいさの問題を解決できるようにも思えますが、実際にはそうではありません。CObject にはメンバ データがないので、仮想継承を使わなくても、基本クラスのメンバ データのコピーが重複することはありません。先ほどの最初の例では、Dump 仮想メソッドの実装方法が CFrameWnd と CObList で異なるため、このメソッドはあいまいなままです。あいまいさを解消する最良の方法は、前のセクションで取り上げた推奨事項を守ることです。
CObject::IsKindOf および実行時の型指定
MFC が CObject でサポートする実行時型指定機構では、DECLARE_DYNAMIC、IMPLEMENT_DYNAMIC、DECLARE_DYNCREATE、IMPLEMENT_DYNCREATE、DECLARE_SERIAL、IMPLEMENT_SERIAL の各マクロを使います。これらのマクロは、安全なダウンキャストを行うために実行時に型をチェックします。
これらのマクロがサポートしているのは単一の基本クラスだけなので、多重継承クラスの場合は使い方が限定されます。IMPLEMENT_DYNAMIC または IMPLEMENT_SERIAL で指定する基本クラスは、最初の、つまり一番左端の基本クラスとして記述してください。このような書き方をすれば、左端の基本クラスだけについて型チェックを行えばよいことになります。実行時型指定方式では、他の基本クラスについてはチェックしません。実行時システムが CFrameWnd の型チェックを行い、CObList はチェックしない例を次に示します。
class CListWnd : public CFrameWnd, public CObList
{
DECLARE_DYNAMIC(CListWnd)
...
};
IMPLEMENT_DYNAMIC(CListWnd, CFrameWnd)
CWnd およびメッセージ マップ
MFC のメッセージ マップ システムを正しく機能させるには、以下の 2 つの条件を満たす必要があります。
CWnd から派生させた基本クラスが 1 つだけである。
CWnd から派生させた基本クラスが最初つまり左端の基本クラスになっている。
次に、正しく機能しない例を示します。
class CTwoWindows : public CFrameWnd, public CEdit
{ ... };
// error : two copies of CWnd
class CListEdit : public CObList, public CEdit
{ ... };
// error : CEdit (derived from CWnd) must be first
多重継承を使ったサンプル プログラム
次のサンプルは、CFrameWnd および CWinApp からの派生クラスが 1 つだけのスタンドアロン アプリケーションです。これはあくまでも、1 つのクラスだけを使用する最小の MFC アプリケーションの例であり、このような方法でアプリケーションを作成することをお勧めするわけではありません。
#include <afxwin.h>
class CHelloAppAndFrame : public CFrameWnd, public CWinApp
{
public:
CHelloAppAndFrame()
{ }
// Necessary because of MI disambiguity
void* operator new(size_t nSize)
{ return CFrameWnd::operator new(nSize); }
void operator delete(void* p)
{ CFrameWnd::operator delete(p); }
// Implementation
// CWinApp overrides
virtual BOOL InitInstance();
// CFrameWnd overrides
virtual void PostNcDestroy();
afx_msg void OnPaint();
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CHelloAppAndFrame, CFrameWnd)
ON_WM_PAINT()
END_MESSAGE_MAP()
// because the frame window is not allocated on the heap, we must
// override PostNCDestroy not to delete the frame object
void CHelloAppAndFrame::PostNcDestroy()
{
// do nothing (do not call base class)
}
void CHelloAppAndFrame::OnPaint()
{
CPaintDC dc(this);
CRect rect;
GetClientRect(rect);
CString s = "Hello, Windows!";
dc.SetTextAlign(TA_BASELINE | TA_CENTER);
dc.SetTextColor(::GetSysColor(COLOR_WINDOWTEXT));
dc.SetBkMode(TRANSPARENT);
dc.TextOut(rect.right / 2, rect.bottom / 2, s);
}
// Application initialization
BOOL CHelloAppAndFrame::InitInstance()
{
// first create the main frame
if (!CFrameWnd::Create(NULL, "Multiple Inheritance Sample",
WS_OVERLAPPEDWINDOW, rectDefault))
return FALSE;
// the application object is also a frame window
m_pMainWnd = this;
ShowWindow(m_nCmdShow);
return TRUE;
}
CHelloAppAndFrame theHelloAppAndFrame;