Java、C++、Eiffel における仮想関数の再定義・多重定義


普段、Smalltalk で遊んでいるだけだと、メンタルモデルが単純になりすぎていけません(^_^;)。 ボケ防止を兼ねて、基本である(…と個人的に考える)上記三言語における継承の例を書いて動かしてみたので結果をメモ。興味の対象は、派生クラスでの同名メンバ関数の定義が、オーバーライドなのかオーバーロード(継承関係をまたいだ…)なのか、それらのコールはどんなふうになされるのか…というごく初歩的なところです。あしからず。


Java
class B {
   int i;

   B(int i) { this.i = i; };

   public boolean eq(B o) {
      System.out.print("B#eq(B) ");
      return i == o.i;
   }
}

class D extends B {
   boolean b;

   D(int i, boolean b) { super(i); this.b = b; };

   public boolean eq(D o) {
      System.out.print("D#eq(D) ");
      return super.eq(o) && b == o.b;
   }

   public boolean eq(B o) {
      System.out.print("D#eq(B) ");
      D dp;
      if(super.eq(o)) {
         try {
            dp = (D)o;
            return  b == dp.b;
         } catch(Exception e) { }
      }
      return false;
   } //*/
}

public class Main {
   public static void main(String[] args) {
      B b1 = new B(1);
      B b2 = new B(2);
      D d1 = new D(1, true);
      D d2 = new D(1, false);
      B bd1 = d1;
                                    //   D#eq(B) あり                 なし
                                    //   ==========================   ================
      System.out.println( b1.eq(b1) );   //=> B#eq(B) true            同左
      System.out.println( b1.eq(b2) );   //=> B#eq(B) false           同左
      System.out.println( d1.eq(d1) );   //=> D#eq(D) B#eq(B) true    同左
      System.out.println( d1.eq(d2) );   //=> D#eq(D) B#eq(B) false   同左
     
      System.out.println( b1.eq(d1) );   //=> B#eq(B) true            同左
      System.out.println( b1.eq(d2) );   //=> B#eq(B) true            同左
      System.out.println( d1.eq(b1) );   //=> D#eq(B) B#eq(B) false   => B#eq(B) true
      System.out.println( d1.eq(b2) );   //=> D#eq(B) B#eq(B) false   => B#eq(B) false
     
      System.out.println( b1.eq(bd1) );  //=> B#eq(B) true            同左
      System.out.println( b2.eq(bd1) );  //=> B#eq(B) false           同左
      System.out.println( d1.eq(bd1) );  //=> D#eq(B) B#eq(B) true    => B#eq(B) true
      System.out.println( d2.eq(bd1) );  //=> D#eq(B) B#eq(B) false   => B#eq(B) true
      System.out.println( bd1.eq(b1) );  //=> D#eq(B) B#eq(B) false   => B#eq(B) true
      System.out.println( bd1.eq(b2) );  //=> D#eq(B) B#eq(B) false   => B#eq(B) false
      System.out.println( bd1.eq(d1) );  //=> D#eq(B) B#eq(B) true    => B#eq(B) true
      System.out.println( bd1.eq(d2) );  //=> D#eq(B) B#eq(B) false   => B#eq(B) true
   }
}


手始めは Java 。ルールは“危惧”していた(理解できるのだろうか…とw)ほど入り組んだものではなくシンプルで、継承ツリー内で同名のメンバ関数は原則としてオーバーロード、引数の型が基底クラスのものと一致するなど限られた状況でのみオーバーライド…と見なされるようです(超いいかげんな理解w)。とはいえ、オーバーロードのない Smalltalk で、関数名(セレクタ)だけでオーバーライドを判断している自分には、頭のスイッチの切換えが必要でした。

これと関係するような関係しないようなところで、bd1.eq(d1) や bd1.eq(d2) で、引数のオブジェクト型 D が一致する D#eq(D) ではなく、D#eq(B) がコールされるところも、最初、理解に苦しみました。D#eq(B) が定義されていないときに、 D#eq(D) ではなく B#eq(B) がコールされるところを見るまでは…(分かってしまえば当たり前のことなんですが(^_^;))。


C++
#include <iostream>
using namespace std;

class B {
protected:
   int i;

public:
   B(int i) : i(i) { };
   virtual ~B() { };
 
   virtual bool operator==(const B& o) {
      cout << "B::==(B&) ";
      return i == o.i;
   }
};

class D : public B {
protected:
   bool b;
   
public:
   D(int i, bool b) : B(i), b(b) { };
   virtual ~D() { };
   
   virtual bool operator==(const D& o) {
      cout << "D::==(D&) ";
      return B::operator==(o) && b == o.b;
    }

//   using B::operator==;   /*

   virtual bool operator==(const B& o) {
      cout << "D::==(B&) ";
      if (B::operator==(o)) {
         const D* dp = dynamic_cast<const D*>(&o);
         if (dp) return b == dp->b;
      }
      return false;
   } //*/
};

int main() {
   B b1(1), b2(2);
   D d1(1, true), d2(1, false);
   B* bd1p = &d1;

                                      //   D#==(B&) あり           なし           using B::==
                                      //   =====================   ===========   =============
   cout << ( b1 == b1 ) << endl;      //=> B::==(B&) 1             同左
   cout << ( b1 == b2 ) << endl;      //=> B::==(B&) 0             同左
   cout << ( d1 == d1 ) << endl;      //=> D::==(D&) B::==(B&) 1   同左
   cout << ( d1 == d2 ) << endl;      //=> D::==(D&) B::==(B&) 0   同左
  
   cout << ( b1 == d1 ) << endl;      //=> B::==(B&) 1             同左
   cout << ( b1 == d2 ) << endl;      //=> B::==(B&) 1             同左
   cout << ( d1 == b1 ) << endl;      //=> D::==(B&) B::==(B&) 0   compile err   => B::==(B&) 1
   cout << ( d1 == b2 ) << endl;      //=> D::==(B&) B::==(B&) 0   compile err   => B::==(B&) 0
  
   cout << ( b1 == (*bd1p) ) << endl; //=> B::==(B&) 1             同左
   cout << ( b2 == (*bd1p) ) << endl; //=> B::==(B&) 0             同左
   cout << ( d1 == (*bd1p) ) << endl; //=> D::==(B&) B::==(B&) 1   compile err   => B::==(B&) 1
   cout << ( d2 == (*bd1p) ) << endl; //=> D::==(B&) B::==(B&) 0   compile err   => B::==(B&) 1
   cout << ( (*bd1p) == b1 ) << endl; //=> D::==(B&) B::==(B&) 0   B::==(B&) 1
   cout << ( (*bd1p) == b2 ) << endl; //=> D::==(B&) B::==(B&) 0   B::==(B&) 0
   cout << ( (*bd1p) == d1 ) << endl; //=> D::==(B&) B::==(B&) 1   B::==(B&) 1
   cout << ( (*bd1p) == d2 ) << endl; //=> D::==(B&) B::==(B&) 0   B::==(B&) 1

   return 0;
}


C++ では、Java のルールに加えて“隠蔽”が絡んできます。派生クラスのメンバが、基底クラス中の同じ名前を隠してしまうという C++ の古くからの“問題”(ストラウストラップ言うところの…)です。このため、D::operator==(D&) が定義された場合、D のインスタンスからは B::operator==(B&) が見えなくなり、Java ではコール可能ないくつかの組み合わせがコンパイルエラーになります。バカ往くを読んでいなかったら、ワケが分からず面食らったことでしょう。


Java と同じ振る舞いをさせるには、素直に派生クラスでも D::operator==(B&) をオーバーライドするか、using B::operator==; と宣言する必要があるようです。

Eiffel
class B create b_make feature

   i: INTEGER;

   b_make(new_i: INTEGER) is
      do
         i := new_i
      end;

   eq(o: like Current): BOOLEAN is
      do
         io.put_string("{B}.eq ");
         Result := (o.i = i)
      end;

end
class D inherit B redefine eq create d_make feature

   b: BOOLEAN;

   d_make(new_i: INTEGER; new_b: BOOLEAN) is
      do
         i := new_i;
         b := new_b
      end;
  
   eq(o: like Current): BOOLEAN is
      do
         io.put_string("{D}.eq ");
         Result := Precursor(o) and (o.b = b)
      end;

end
class MAIN create make feature

   make is
      local
         b1, b2, bd1: B;
         d1, d2: D
      do
         create b1.b_make(1);
         create b2.b_make(2);
         create d1.d_make(1, True);
         create d2.d_make(1, False);
         bd1 := d1;

         io.put_boolean( b1.eq(b1) ); io.put_new_line;  -- => {B}.eq True
         io.put_boolean( b1.eq(b2) ); io.put_new_line;  -- => {B}.eq False
         io.put_boolean( d1.eq(d1) ); io.put_new_line;  -- => {D}.eq {B}.eq True
         io.put_boolean( d1.eq(d2) ); io.put_new_line;  -- => {D}.eq {B}.eq False

         io.put_boolean( b1.eq(d1) ); io.put_new_line;  -- => {B}.eq True
         io.put_boolean( b1.eq(d2) ); io.put_new_line;  -- => {B}.eq True
      -- io.put_boolean( d1.eq(b1) ); io.put_new_line;  -- => compile err
      -- io.put_boolean( d1.eq(b2) ); io.put_new_line;  -- => compile err

         io.put_boolean( b1.eq(bd1) ); io.put_new_line; -- => {B}.eq True
         io.put_boolean( b2.eq(bd1) ); io.put_new_line; -- => {B}.eq False
      -- io.put_boolean( d1.eq(bd1) ); io.put_new_line; -- => compile err
      -- io.put_boolean( d2.eq(bd1) ); io.put_new_line; -- => compile err
      -- io.put_boolean( bd1.eq(b1) ); io.put_new_line; -- => runtime err
      -- io.put_boolean( bd1.eq(b2) ); io.put_new_line; -- => runtime err
         io.put_boolean( bd1.eq(d1) ); io.put_new_line; -- => {D}.eq {B}.eq True
         io.put_boolean( bd1.eq(d2) ); io.put_new_line; -- => {D}.eq {B}.eq False
      end;

end


Eiffel にはオーバーロードがないので、基底クラス B の仮想関数(Eifflel では「ルーチン」…)と同名のメンバ関数が派生クラス D で定義された場合、原則としてオーバーライド扱いになります(ある意味、Smalltalk 的なお気軽さ)。というか、手続き(?)上、オーバーライドを明示的に(redefine 宣言)しないかぎり、派生クラスに同名のメンバ関数を存在させることができません。また、隠蔽された基底クラスの同名メンバ関数を派生クラスのインスタンスからのコールしたい場合は、別名として宣言(rename 元の名前 as 別名)しておきます。Ruby で super が使えないときの方法(alias を使う)と発想が似ていますね。

…と書いたのを読んでいて気がついたのですが、Ruby の super が SmalltalkJava の super とは違う、ちょっと変わった仕様になっているのは、名前はともかく、機能面でのルーツが、この Eiffel にある Precursor() だったからなのかも。…と書いたのを読んでいたら、Ruby に Eiffel 以上に大きな影響を与えている CLOS の存在をすっかり忘れていることに気が付きました(^_^;)。そう。call-next-method 。これだ。 Precursor() からだと引数を省略できる、という発想には至りにくいし…。ということでググると、そのものズバリのご本人の言及が見つかりました。→[ruby-list: 3209]


閑話休題


クックが指摘("A Proposal for Making Eiffel Type-safe" [CookEiffel89.pdf])しているように、基底クラス型の変数に関連づけされた派生クラスのインスタンスを介して eq(like Current) をコールするような場合、コンパイラは {B}.eq(B) と解釈して通すのに、ランタイム時には {D}.eq(D) をコールして失敗する型システムの“穴”も見受けられます(もっとも、ここのはその穴を突くための意地悪な例なわけですが…)。


今は、Eiffel もクックの頃とは違って進化し、再三指摘され続けたこの種の問題に対処可能なようになっています。たとえば、引数を共変させずに次のように書けば、ランタイムエラーを回避しながら JavaC++ と同じような振る舞いをさせられます。

class B create b_make feature

   i: INTEGER;

   b_make(new_i: INTEGER) is
      do
         i := new_i;
      end;

   eq(o: B): BOOLEAN is
      do
         io.put_string("{B}.eq ");
         Result := (o.i = i);
      end;
  
end
class D inherit B redefine eq create d_make feature

   b: BOOLEAN;

   d_make(new_i: INTEGER; new_b: BOOLEAN) is
      do
         i := new_i;
         b := new_b
      end;

   eq(o: B): BOOLEAN is
      local
         d: D;
      do
         io.put_string("{D}.eq ");
         if Precursor(o) and d ?:= o then
            d ::= o;
            Result := (d.b = b);
         else
            Result := False;
         end;
      end;

end
class MAIN create make feature

   make is
      local
         b1, b2, bd1: B;
         d1, d2: D;
      do
         create b1.b_make(1);
         create b2.b_make(2);
         create d1.d_make(1, True);
         create d2.d_make(1, False);
         bd1 := d1;

         io.put_boolean( b1.eq(b1) ); io.put_new_line;  -- => {B}.eq True
         io.put_boolean( b1.eq(b2) ); io.put_new_line;  -- => {B}.eq False
         io.put_boolean( d1.eq(d1) ); io.put_new_line;  -- => {D}.eq {B}.eq True
         io.put_boolean( d1.eq(d2) ); io.put_new_line;  -- => {D}.eq {B}.eq False

         io.put_boolean( b1.eq(d1) ); io.put_new_line;  -- => {B}.eq True
         io.put_boolean( b1.eq(d2) ); io.put_new_line;  -- => {B}.eq True
         io.put_boolean( d1.eq(b1) ); io.put_new_line;  -- => {D}.eq {B}.eq False
         io.put_boolean( d1.eq(b1) ); io.put_new_line;  -- => {D}.eq {B}.eq False

         io.put_boolean( b1.eq(bd1) ); io.put_new_line; -- => {B}.eq True
         io.put_boolean( b2.eq(bd1) ); io.put_new_line; -- => {B}.eq False
         io.put_boolean( d1.eq(bd1) ); io.put_new_line; -- => {D}.eq {B}.eq True
         io.put_boolean( d2.eq(bd1) ); io.put_new_line; -- => {D}.eq {B}.eq False
         io.put_boolean( bd1.eq(b1) ); io.put_new_line; -- => {D}.eq {B}.eq False
         io.put_boolean( bd1.eq(b2) ); io.put_new_line; -- => {D}.eq {B}.eq False
         io.put_boolean( bd1.eq(d1) ); io.put_new_line; -- => {D}.eq {B}.eq True
         io.put_boolean( bd1.eq(d2) ); io.put_new_line; -- => {D}.eq {B}.eq False
      end;

end
おまけ(Smalltalk で直訳気味に…。メタ情報を除いた加工コード(ファイルイン不可。ブラウザへのコピペが必要)にて)
Object subclass: #B
    instanceVariableNames: 'i'

B class >> i: i
   ^ self new setI: i; yourself

B >> setI: newI   " private "
   i := newI

B >> i
   ^ i

B >> = o
   Transcript show: 'B>>#= '.
   ^ o i = i
B subclass: #D
    instanceVariableNames: 'b'

D calss >> i: i b: b
   ^ self new setI: i b: b; yourself

D >> setI: newI b: newB   " private "
   i := newI.
   b := newB

D >> b
   ^ b

D >> = o
   Transcript show: 'D>>#= '.
   ^ super = o and: [(o isKindOf: D) and: [o b = b]]
| b1 b2 d1 d2 |
b1 := B i: 1.
b2 := B i: 2.
d1 := D i: 1 b: true.
d2 := D i: 1 b: false.

Transcript open;
   cr; show: b1 = b1;   " => B>>#= true "
   cr; show: b1 = b2;   " => B>>#= false "
   cr; show: d1 = d1;   " => D>>#= B>>#= true "
   cr; show: d1 = d2;   " => D>>#= B>>#= false "

   cr; show: b1 = d1;   " => B>>#= true "
   cr; show: b1 = d2;   " => B>>#= true "
   cr; show: d1 = b1;   " => D>>#= B>>#= false "
   cr; show: d1 = b2    " => D>>#= B>>#= false "


ただ、Smalltalk の場合、B >> #=、D >> #= のコールに際し、その引数に何がくるか分かったものではないので、上の言語たちを直訳したこの方法では、静的型チェックに期待できないぶん、著しく網羅性に欠けたものになってしまっています。


次のように書き直せば、いくぶんマシかと。

B >> = o
   Transcript show: 'B>>#= '.
   ^ self class = o class and: [o i = i]
D >> = o
   Transcript show: 'D>>#= '.
   ^ super = o and: [o b = b]


B >> #= の型チェック(self class = o class)は、D >> #= で super = o によりコールされた場合にも有効なので、D >> #= にあった #isKndOf: による型チェックは不要になります。ただしこれと同時に、レシーバと引数のクラスが異なる場合、#= が true を返すことはなくなるため、“仕様”も変わってしまうのですが…。