菱形継承問題の再考と整理

菱形継承問題というと、多重継承が槍玉にあげられる際の筆頭ですが、なんとなく wikipedia を見てみたら酷いことになってたので、ちょっとなんか書きます。
言語 independent な記述にするために微妙な単語のチョイスがあったりするかもしれませんがフィーリングでなんとかしてください。言語 dependent な部分は多分横文字じゃない感じになると思います。

まず、所謂菱形継承問題が生じる条件を整理します。

これによって生じる問題は三つあります。一つずつ見ていきましょう。

B と C は A のサブクラスなので A の状態やら何やら、つまり値表現*1を持っているわけですが、D は B と C のそれぞれのために A の値表現を二つ持たないといけないのか、それとも一つにして B と C には同じ値表現を使ってもらえばいいのか、という問題があります。
C++ は普通に継承してると前者に、仮想継承というニッチな言語機能を利用すると後者になります。やあ、よくよく考えられた言語ですね C++ は…
一方他の言語は大体後者しか選べない気がします。いや前者しか選べない言語もあるよ、両方選べる言語他にもあるよ、とかあったら教えてください。言語毎の事情調べるのサボってかいてます。

兎に角、値表現を共有するか否かという問題があるわけです。

  • メソッド呼び出し時の解決が曖昧になる

ようは D のメソッドを読んだときに B と C に同じシグネチャのメソッドがあるので解決する時に困るね、という話です。二番目に持ってきましたが、多分これが一般的な菱形継承問題の認識なんじゃないかなあと思います。
これは、指定する方法があるなら指定するなり、呼び出したい方のクラスにアップキャストするなりで回避できます。

しかしこの問題は、実のところ菱形継承特有のものではありません。兎に角、同じシグネチャのメソッドを持つ複数のスーパークラスがあればいいのだから、多重継承に因る問題です。

  • メソッド定義時の解決が曖昧になる

曖昧になるのは呼び出し側だけではありません。当然、メソッドを定義する側でも起こり得ます。
例えば、同じシグネチャのメソッドを持つクラスを複数スーパークラスとして持つクラス内で、スーパークラス毎のメソッドはどのように扱われるでしょうか?
C++ では区別されません、というよりはできません(確か…)。そのため、スーパークラス毎にメソッドをオーバーライドする、というようなことはできません。vtbl は普通クラス毎に存在するはずなので、不思議な話ですね。仕様ではその辺の実装まで踏み込んでないからかな。まあオーバーライドは別個に別のクラスで行ってそのクラスを継承する形に書き換えれば解決できます。

さて、この三つの問題は、全てが菱形継承ないし多重継承固有の問題なのでしょうか?実はそうでもありません。

まずメソッド定義時の解決について。Java の interface では、同じシグネチャを持つメソッドは、異なる interface で宣言されていたとしても、区別できません(確か…)。C++ と同じですね(未確認なのも含めて…)。
一方で、C# の場合は区別できます、というか区別しないといけないんだったかな…同じシグネチャのメソッドでも、別の interface で宣言ないし定義されているのならば、それは別物であるということです。
しかし、これが区別できてしまうと、前述のメソッド呼び出し時の解決の曖昧性の問題が生じます。Java の場合は、そもそも定義側で区別ができない、同じものとして扱われているのだから、呼び出し側で区別をする必要がそもそも生じません。ところが C# の場合は区別できてしまう、区別しなければならないわけです。解決方法は矢張りアップキャストです。

あまりまとまりませんでしたが、菱形継承問題について今更なんか書きました。場合によっては、問題の一部は多重継承のない言語でも発生するんですよ、というおまけつき。C++, Java, C# 以外の言語は reference なしにちょっと語る自身がなかったので触れていません。仕様とかちゃんと確認してないので、嘘があったら教えて貰えると嬉しいです。他の言語での事情は、詳しい人がいたら教えてください。Python の C3 linearization とかその辺…まあ Scala の linearization と大体同じかなーと勝手に思ってるんですが。

ここにメッセージベースの OO とかプロトタイプベースの OO とか入ってくると多分わけが分からなくなるので、それはもう勘弁してください。うう、こんなもの書いてる場合じゃないし書くんじゃなかった…疲れた…

おしまい。

*1:この言葉は今作ったので読み終わったら忘れてよい