1 ダイアモンド継承の問題

 クラスAから派生したクラスA1とA2があるとして、このA1とA2を多重継承したクラスBを作った場合、このような継承の形を(継承のパスがひし形になる事から)「ダイアモンド継承」と呼びます*1

 話はちょっとずれてキャストという物についてちこっと説明します(後でダイアモンド継承につながります)
 クラスAを継承したクラスA'からインスタンスiA'を作ったとします。
 このインスタンスiA'をクラスAにアップキャストしてポインタpAに代入したとします。 以後、ポインタpAは、クラスA'のインスタンスである筈のiA'を、さもクラスAのインスタンスであるかのように振舞う事ができます。これはオブジェクト指向プログラミングの特徴であるポリモーフィズムの一つです。

 さて、言語仕様としてこれが出来るのはわかるのですが、現実的に「何故」このような事ができるのでしょうか? ポインタpAは中に入っているのがAなのかA'なのかを判断したりはしません。というかそもそもそんな事はで来ません。ポインタpAにAが入るのかA'が入るのか、コンパイル段階、つまりpAの挙動が決定される段階では判断出来ないからです。

 別の例を挙げます。クラスAとクラスBから多重継承したクラスABのインスタンスiABは、クラスAにもクラスBにもアップキャスト、ダウンキャストする事が出来ます。何故、そんな事が出来るのでしょうか?

 これ、別にクイズでないので驚くべき回答は用意されていません(笑)。以下に解説します。

 クラスのインスタンスが生成されると、ヒープメモリ領域にそのインスタンスのメンバ変数の領域が確保されます*2。この時、クラスYがクラスX,クラスZを継承している場合、以下のように領域が確保されます。左端の数字は先頭からのオフセットです*3

+0 クラスYの変数1
  クラスYの変数2
  クラスYの変数3
  .
  .
+n クラスXの変数1
  クラスXの変数2
+n+mクラスZの変数1
  クラスZの変数2

 生成されたインスタンスのアドレスは、ポインタに代入されますが、そのアドレスは上で示す"+0"のアドレスになります。
 さて、アップキャスト、ダウンキャストという行為はこのメンバ変数の開始位置をずらす処理の事を差します。クラスYのインスタンスをクラスXにアップキャストしたい場合、ポインタを+nすればあたかもクラスXのように振舞う事が出来ます。クラスZにアップキャストしたい場合は+n+mすれはOKです。ダウンキャストはこの逆でやっぱり単純にアドレスをマイナスさせるだけです*4

 さてさて長くなってしまいました。ダイアモンド継承に移りましょう。ダイアモンド継承の例をもう一度示します。

クラスBは、クラスA1とA2を多重継承している。
A1,A2は共にクラスAからの派生クラスである。

 既におわかりかと思いますが、単純にダイアモンド継承が実現できてしまった場合、先ほどのインスタンス化のルールにより、クラスBのインスタンスは、クラスAのメンバ変数を二つ持つ事になってしまいます。外からそのメンバ変数を呼び出した場合、「どちらの」クラスAのメンバ変数を参照すればいいのかわからない為、コンパイルエラーになります。

 これを防ぐ為に、C++では親クラス(ここではクラスA)を仮想基本クラスとして継承する事によって、A1とA2の親クラスを1つの物とみなすようにします*5

*1:これ本当は嘘です。実際には「単純に継承をしてもダイアモンド継承は作れないので、親クラスを仮想基本クラスにする必要がある」という話になります。ですからここで話しているのは実際にはダイアモンド継承の話ではありません^^;

*2:基本的事項をあえて確認しておきますが、インスタンスがいくつ生成されても、メンバ関数には影響されません。メンバ関数インスタンスがいくら増えようと1つあればよく、また、絶対アドレスがコンパイル段階で確定されるからです。

*3:話を分かりやすくするためvtableを無視しています。ご勘弁下さい。

*4:ここから、一般的に言われる「ダウンキャストは気をつけろ」という教訓が分かります。アップキャスト、ダウンキャストは単なるポインタのオフセット加減算に過ぎないため、コンパイラはあるポインタAがダウンキャストしてよいのか、つまり、そのポインタがアップキャストされた物なのかを判断する事が出来ないのです

*5:これで本当のダイアモンド継承が実現できました。いいかどうかはわかりませんが……