Mix-in

最近(?)のオブジェクト指向の流れの一つにMix-inという技術があるのですが、いまいちなんの役に立つのか良くわかってませんでした。言語によって"Mix-in"自体の意味も異なるようだし。で、最近Rubyの本読んでてこのMix-inの説明があり、自分的に納得できる物だったので、覚書しておきたいと思います(長い前フリだな)。

 あくまでRubyでの話なので、Mix-JuceでのMix-inとは異なるのかもしれませんがしらない(なげやり)。また、あくまでみはえる解釈なので本来Rubyが主張するMix-inとは異なっているかもしれません(すまそ)

Mix-inは、そもそもLisp界において、多重継承がもたらす複雑さや障害を回避する為に考案された「実装上の技術」の事でした。その後、他の言語には言語仕様として取り入られていくようです。

ではまず、多重継承が抱える問題について説明して行きましょう

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:これで本当のダイアモンド継承が実現できました。いいかどうかはわかりませんが……

2名前衝突

 これは多重継承とは別に存在する問題だと思うのですが一応
 複数のクラスを継承して新しいクラスを作りたいとします。この時、個々のクラスにおいて同じ名前のメンバがあった場合、名前衝突になります。実際問題として、外部からこのメソッドを呼び出しても、どっちのクラスのメソッドが呼び出されたことになるの判断できません(試してませんがコンパイル出来ないと思います)

3複雑さ

 1,2を見てもらえばお分かりかと思いますが、多重継承は(プログラマ)にとって大変複雑な物です。ただでさえツリーのように継承図が出来てしまう上に、ダイアモンド継承*1があると継承図がグラフ化してしまうためより複雑になってしまいます。正直、誰かが親クラスをいじってたりしたら、とても保守しきれません(あ、本音が)。

*1:これは本当の意味でのダイアモンド継承

4愚痴(笑)

C++やってりゃわかると思うんですが、多重継承で実装を再利用するタイプのコードを書いてると、インターフェイスベースのJavaと違って、実装が散逸してしまって非常に可読性が低いコードが出来上がってしまいます。え? 俺が悪いの?(笑)

で、なにがいいたいのかと言うと

 1で書いたダイアモンド継承の問題は、読んでいただければわかるとおり「ダイアモンド継承が起きる(または起きない)危険性をどこかで処理しなければならない」という点にあります。しかし、これは実装上の問題であって、プログラマ考慮する事では無いと思います。

 とまあここまで来て、では多重継承の代替手段はあるのか? という話になります(続く)