ホーム » 「多重継承」タグがついた投稿

タグアーカイブ: 多重継承

2019年9月
« 8月    
1234567
891011121314
15161718192021
22232425262728
2930  

最近の投稿(電子情報)

アーカイブ

カテゴリー

複雑な継承

課題で取り組んでもらっている、動物の進化を表すクラスの概要を示す。

動物・鳥類・哺乳類クラス

// 動物クラス
class Animal {
private:
  char name[ 10 ] ;
public:
  Animal( const char s[] ) {
    strcpy( name , s ) ;
  }
  const char* get_name() const { return name ; }
  virtual void move() = 0 ;
  virtual void birth() = 0 ;
} ;

// 鳥類クラス
class Bird : public Animal {
public:
  Bird( const char s[] ) : Animal( s ) {}
  virtual void move() {
    printf( "%s fry.\n" , get_name() ) ;
  }
  virtual void birth() {
    printf( "%s lay egg.\n" , get_name() ) ;
  }
} ;

// 哺乳類クラス
class Mammal : public Animal {
public:
  Mammal( const char s[] ) : Animal( s ) {}
  virtual void move() {
    printf( "%s walk.\n" , get_name() ) ;
  }
  virtual void birth() {
    printf( "%s lay baby.\n" , get_name() ) ;
  }
} ;

int main() {
  Bird chiken( "piyo" ) ;
  chiken.move() ;
  chiken.birth() ;
  Mammal cat( "tama" ) ;
  cat.move() ;
  cat.birth() ;
  return 0 ;
}

ここで、カモノハシを作るのであれば、どうすれば良いだろうか?

鳥類・哺乳類とは別にカモノハシを作る

class SeaBream : public Animal {
public:
  Mammal( const char s[] ) : Animal( s ) {}
  virtual void move() {
    printf( "%s walk.\n" , get_name() ) ;
  }
  virtual void birth() {
    printf( "%s lay egg.\n" , get_name() ) ;
  }
} ;

この例では、簡単な処理だが、move() の中身が複雑であれば、改めて move() を宣言するのではなく、継承するだけの書き方ができないだろうか?

多重継承

C++ には、複数のクラスから、派生する多重継承という機能がある。であれば、鳥類と哺乳類から進化したのだから、以下のように書きたい。

class SeaBream : public Bird , Mammal {
} ;

しかし、カモノハシに move() を呼び出すと、鳥類の move() と哺乳類の move() のどちらを動かすか曖昧になる。また、派生クラスは親クラスのデータ領域と、派生クラスのデータ領域を持つため、鳥類の name[] と、哺乳類の name[] を二つ持つことになる。

足と羽のクラス

class Animal {
private:
  char name[ 10 ] ;
public:
  Animal( const char s[] ) {
    strcpy( name , s ) ;
  }
  const char* get_name() const { return name ; }
  virtual void move() = 0 ;
} ;
// 羽
class Wing {
public:
   const char* move_method() { return "fly" ; }
} ;
// 
class Leg {
public:
   const char* move_method() { return "walk" ; }
} ;
class Bird : public Animal , Wind {
public:
  Bird( const char s[] ) : Animal( s ) {}
  virtual void move() {
    printf( "%s %s.\n" , get_name() , move_method() ) ;
  }
} ;
class Mammal : public Animal , Leg {
public:
  Mammal( const char s[] ) : Animal( s ) {}
  virtual void move() {
    printf( "%s %s.\n" , get_name() , move_method() ) ;
  }
} ;

# うーむ、継承する処理が1行程度でかける処理だと、どのやり方も「継承が便利」というように見えないな…(x_x;

class Animal {
private:
   char name[ 10 ] ;
public:
   Animal( const char s[] ) {
      strcpy( name , s ) ;
   }
   const char* get_name() const { return name ; }
   virtual void move() = 0 ;
   virtual void birth() = 0 ;
} ;

// 鳥類クラス
class Bird : public virtual Animal {
public:
   Bird( const char s[] ) : Animal( s ) {}
   virtual void move() {
      printf( "%s fry.\n" , get_name() ) ;
   }
   virtual void birth() {
      printf( "%s lay egg.\n" , get_name() ) ;
   }
} ;

// 哺乳類クラス
class Mammal : public virtual Animal {
public:
   Mammal( const char s[] ) : Animal( s ) {}
   virtual void move() {
      printf( "%s walk.\n" , get_name() ) ;
   }
   virtual void birth() {
      printf( "%s lay baby.\n" , get_name() ) ;
   }
} ;

class SeaBream : public virtual Bird , virtual Mammal {
public:
   SeaBream( const char s[] ) : Animal( s ) {}
   void move() {
      Mammal::move() ;
   }
   void birth() {
      Bird::birth() ;
   }
} ;

ただし、多重継承は親クラスの情報と、メソッドを継承する。この場合、通常だと name[] を二つ持つことになるので、問題が発生する。そこで、親クラスの継承に virtual を指定することで、ダイヤモンド型継承の 2つの属性をうまく処理してくれるようになる。

しかし、多重継承は処理の曖昧さや効率の悪さもあることから、採用されていないオブジェクト指向言語も多い。特に Java は、多重継承を使えない。その代わりに interface という機能が使えるようになっている。

仮想関数を用いた課題

第2回レポート課題

  1. 純粋仮想基底クラスの資料を参考に、複素数データ(直行座標系でも極座標でもよい)の並び替えを行うプログラムを作成せよ。ただし、(1)複素数専用の並び替え関数を作らないこと。(2)複素数用の比較関数を作ること。(3)Object型の並び替え my_sort() を使うこと。
  2. 生物を表す基底を作成し、以下の機能を持つ派生クラスを作成せよ。
    1. 生物クラスは、<名前>を持つ。
    2. 哺乳類クラス(Mammal)に、move() を実行すると、”<名前>は歩く”と表示すること。
    3. 哺乳類クラスに、spawn() を実行すると、”<名前>は子供を産む”と表示すること。
    4. 鳥クラス(Bird)に、move() を実行すると、”<名前>は飛ぶ”と表示すること。
    5. 鳥クラスに、spawn() を実行すると、”<名前>は卵を産む”と表示すること。
    6. 人間クラス(Human)に move() を実行すると、”<名前>は歩く”と表示されること。
    7. 人間クラスに spawn() を実行すると、”<名前>は子供を産む”と表示されること。
    8. にわとりクラス(Chiken)に、move(),spawn() を実行できること。
    9. かものはしクラス(SeaBream)を作るにはどうすればいいか考察せよ。
    class Creature {
    } ;
    class Mammal : ...... {
    } ;
    class Human : ...... {
    } ;
    class Bird : ...... {
    } ;
    class Chiken : ...... {
    } ;
    int main() {
       Mammal tama_cat( "tama" ) ;
       tama_cat.move() ;    // tamaは歩く
       tama_cat.spawn() ;   // tamaは子供を産む
    
       Human  jane_human( "jane" ) ;
       jane_human.move() ;  // janeは歩く
       jane_human.spawn() ; // janeは子供を産む
    
       Bird   tori_bird( "tori" ) ;
       tori_bird.move() ;   // toriは飛ぶ
       tori_bird.spawn() ;  // toriは卵を産む
    
       Chiken piyo_chiken( "PiyoPiyo" ) ;
       piyo_chiken.move() ; // PiyoPiyoは飛ぶ
       piyo_chiken.spawn() ;// PiyoPiyoは卵を産む
    
       SeaBream golduck( "golduck" ) ;
       golduck.move() ;     // golduckは歩く
       golduck.spawn() ;    // golduckは卵を産む
    }
    

図形と仮想関数の継承方法

純粋仮想基底クラスと図形の課題の基本形

課題で取り組んでいるプログラムは、純粋仮想基底クラスFigureと、そこから派生させたクラスと仮想関数で絵を書いている。このような派生の関係を以下のような図で表現する。

class Figure {
public:
   virtual void draw( int x , int y ) = 0 ;
} ;

class FigureBox : public Figure {
private:
   int width , height ;
public:
   FigureBox( int w , int h ) : width( w ) , height( h ) {}
   virtual void draw( int x , int y ) {
      // 四角を描く処理
   }
} ;

class FigureCircle : public Figure {
private:
   int radius ;
public:
   FigureCircle( int r ) : radius( r ) {}
   virtual void draw( int x , int w ) {
      // 円を描く処理
   }
} ;

色付き図形を派生する方法

課題では、上記プログラムを活用して、色付き図形のクラスを作ることを目的とするが、その実装方法には色々な方法がある。

単純なやり方は、FigureBox と同じように、Figure から FigureColorBox を派生させる方法だろう。

class FigureColorBox : public Figure {
private:
   int width , height , color ;
public:
   FigureColorBox( int w , int h , int c )
   : width( w ) , height( h ) , color( c ) {}
   virtual void draw( int x , int y ) {
      // 色を変える処理
      // 四角を描く処理 ... FIgureBox と同じ処理
   }
} ;

この方法は単純だけど、四角を描く処理を書くため無駄であり、FIgureBox と処理を共通化できればプログラムを書く手間を減らせるはず。

処理を共通化するなら派生すればいい

class FigureColorBox : public FigureBox {
private:
   int color ;  // 幅と高さの記述が無い
public:
   FigureColorBox( int w , int h , int c )
   : FigureBox( w , h ) , color( c ) {}
   virtual void draw( int x , int y ) {
      // 色を変える処理
      FigureBox::draw( x , y ) ; // 親クラスの処理を継承
   }
} ;

同じような色の処理を追加したクラスが沢山ある場合

上記の、FigureBox から FigureColorBox を派生させた場合は、四角を描く処理が継承により共通化ができた。

しかし、同じように FigureCircle から FigureColorCircle を派生させる…といったクラスを沢山作る場合は、色を変える処理を何度も使うことになるが、処理の共有することができない。こういった場合は、以下のような方法がある。

class Color {
private:
   int color ;
public:
   Color( int c ) : color( c ) {}
   void set_color() {
      // 色を変える処理
   }
} ;
class FigureColorBox : public FigureBox , public Color {
public:                // 多重継承のキモ
   FigureColorBox( int w , int h , int c )
   : FigureBox( w , h ) , Color( c ) {}
   virtual void draw( int x , int y ) {
      Color::set_color() ;       // Colorクラスを使って色を設定
      FigureBox::draw( x , y ) ; // FigureBoxクラスで形を描く
   }
} ;

多重継承

上記の FigureColorBox のように、親クラスとして、FigureBox と Color のように複数のクラスをもつ継承は、多重継承と呼ばれる。

ただし、多重継承は後に示すような曖昧さや、実装の際の手間を考えると、多重継承は必ずしも便利な機能ではない。このため、オブジェクト指向のプログラミング言語によっては、多重継承機能が使えない。

Java では、多重継承が使えない代わりに、interface 機能が使えたりする。

ダイヤモンド型の継承と曖昧さ

C++ のような言語での多重継承が問題となる事例として、ダイヤモンド型の継承が挙げられる。

例えば、動物クラスから鳥クラス・哺乳類クラスを派生して、鳥クラスからニワトリを派生して、哺乳類クラスから人間を派生するというのは、進化の過程を踏まえると、自然な派生と考えられる。

しかし、鳥がクラスメンバーとして羽と足を持ち、哺乳類が手と足を持つとしたとき、鳥のくちばしを持ち卵を生むカモノハシを派生する場合には、どうすれば良いのだろうか?しかし、これを鳥と哺乳類を親クラスとした多重継承で実装をすると、手と羽と足が4本のカモノハシができてしまう。(お前はドラゴンかッ!)

また、大元の動物クラスがインスタンスを持つ場合、このような多重継承をすると、同じ名前のインスタンスを2つ持つことになる。この場合、C++では仮想継承というメカニズムを使うことができる。

class Animal {
private:
   char  name[10] ;
} ;
class Bird : public virtual Animal {
} ;
class Mammalian : public virtual Animal {
} ;
class Psyduck : public Bird , public Mammalian {
   // このクラスは、name は1つだけ。 
} ;

また、動物が動くためのmove()というメソッドを持つとした場合、鳥は羽で飛び、哺乳類は足で移動する処理となるだろう。しかし、多重継承のカモノハシは、鳥の羽で動くメソッドmove()と、哺乳類の足で動くメソッドmove()を持つことになり、カモノハシに動け…と命令した場合、どちらのメソッドmove() を使うのだろうか?

こういった、使いにくさ・実装時の手間・処理の曖昧さを考慮したうえで、多重継承のメカニズムが使えるオブジェクト指向プログラム言語は少ない。