Effective C++ 入門
〜 第6章 継承とオブジェクト指向設計 〜


□ 35項 □ public に継承するときは、『その一種である』関係のモデルかどうか確認しよう
■ 課題

なんちゃってオブジェクト指向設計では、『その一種である』 ということと、『それを持っている』 ということと、『それを実装手段とする』 の関係をうまく整理できておらず、とてもわかりづらい。

■ 解決

・ 『その一種である』 (is-a関係) - public 継承を使う

・ 『それを持っている』 (has-a関係) - コンポジションを使う

・ 『それを実装手段とする』 (s-implemented-in-terms-of関係) - private 継承を使う



■ 補足

・ protected 継承に意味づけはない

・ プログラミングのモデルが実世界の直感に必ずしも合致するものとは限らない。 「すべてのソフトウェアにとって理想的な唯一の設計」 というものは存在しない。最善の設計とは、そのシステムに現在、そして未来に何が要求されるかによって決まるもの。

・ 詳細の議論については、Effective C++ 35項参照。
□ 36項 □ インターフェースの継承と実装の継承とを区別しよう
■ 課題

なんちゃってプログラマの実装したソフトは、public 継承が、関数の「インターフェースの継承」と関数の「実装の継承」という分離不可能な2つの要素から構成されていることを意識していない ため、とってもわかりづらい。

■ 解決

・ 非仮想関数を宣言する目的は、その関数のインターフェースおよびその強制的な実装を派生クラスに継承させること。

・ 純粋仮想関数を宣言する目的は、派生クラスに関数のインターフェースだけを継承させること。(実装は継承しない)

・ 一般仮想関数を宣言する目的は、派生クラスに関数のインターフェースとデフォルト実装を継承させること。

一般仮想関数で、関数の宣言とデフォルト実装の両方を指定できるようにするのは、危険な場合がある。そのような場合は、デフォルト実装を基底クラスのメンバ関数として定義し、サブクラスで必要に応じて利用できるようにすればよい。以下例。

class Airplane {
public:
__virtual void fly(const Airport& destination) = 0;
protected:
__void defaultFly(const Airport& destination) { /* default fly */ }
};

class ModelA : public Airplane {
public:
__// デフォルトの fly を継承
__virtual void fly(const Airport& destination) { defaultFly(destination); }
};

class ModelB : public Ariplane {
public:
__// カスタムのflyを実装
__virtual void fly(const Airport& destination) { customFly(destination); }
private:
__void customFly(const Airport& destination) { /* custom fly */ }
};

■ 補足

・ オブジェクト指向設計において、すべての関数を非仮想関数とすることも、すべての関数を仮想関数とすることも、たいていは両方間違い。
□ 37項 □ 継承した非仮想関数を再定義してはならない
■ 課題

基底クラスから継承した派生クラスで、基底クラスの非仮想関数と同じ名前の非仮想関数を定義したら、振る舞いがおかしくなった。以下例。

#include <iostream>
using namespace std;

class Base {
public:
__virtual ~Base() {}
__void doIt();
};

class Derrived : public Base {
public:
__virtual ~Derrived() {}
__void doIt();
};

void Base::doIt() {
__cout << "This is Base!" << endl;
}

void Derrived::doIt() {
__cout << "This is Derrived!" << endl;
}

int main() {
__Derrived* d = new Derrived;
__d->doIt(); // Derrived の doIt() が呼び出される
__Base* b = (Base*)d;
__b->doIt(); // Base の doIt() が呼び出される
__delete d;
}

■ 解決

派生クラスの非仮想関数を別の名前をするか、継承関係を見直そう。ちなみに上記プログラムは以下のように仮想関数にすれば問題なく動作する。

.... (snip) .....
class Base {
public:
__virtual ~Base() {}
__virtual void doIt();
};

class Derrived : public Base {
public:
__virtual ~Derrived() {}
__virtual void doIt();
};
.... (snip) .....

■ 補足

36項も参照
□ 38項 □ 継承したデフォルトパラメータ値を再定義してはならない
■ 課題

派生クラスで、基底クラスと異なるデフォルトパラメータを設定したら振る舞いがおかしくなった。以下例。

#include <iostream>
using namespace std;

class Base {
public:
__virtual void doIt(int param = 0);
};

class Derrived : public Base {
public:
__virtual void doIt(int param = 1);
};

void Base::doIt(int param) {
__cout << "param is " << param << endl;
}

void Derrived::doIt(int param) {
__cout << "param is " << param << endl;
}

int main() {
__Derrived* d = new Derrived;
__d->doIt(); // param=1 が呼ばれる
__Base* b = (Base*)d;
__b->doIt(); // param=0 が呼ばれる
__delete d;
}

■ 解決

解決策はない。

デフォルトパラメータは派生クラスで定義してはいけない。

そのような状況となった場合は、継承関係を見直すか、オーバーロードをするべきものでないか、もう一度よく見直そう。

■ 補足

C++がこのような仕様となっているのは、実行効率を重視したためらしい。詳細は、Effective C++ を参照。
□ 39項 □ 継承の階層構造を下る方向のキャスト(ダウンキャスト)は避けよう
■ 課題

ダウンキャストはコードの可読性を著しく低下させ、オーバーヘッドもかかるため、好ましいものではない。

また、static_castを使ったダウンキャストは、型の変換を誤る可能性があり、深刻な問題を発生させる可能性がある。(48項参照)

■ 解決

ダウンキャストを用いるときは、dynamic_cast を用いること。ダウンキャストを除去するために、以下のことを試みること。

・ ダウンキャストをして用いている関数を、基底クラスの仮想関数への置き換えを検討すること。

・ 本来、派生クラスとして扱うべきものではないか、もう一度見直すこと。

■ 補足

とは言っても、なかなかダウンキャストを用いないプログラムを書くのは難しい。しかし、不可能ではない。それを実現するキーがデザインパターンである。デザインパターンに興味があることはこちらをどうぞ。
□ 40項 □ 「それを持っている」関係や「それを実現手段とする」関係は層を重ねる形でモデル化しよう
■ 課題

なんちゃってオブジェクト指向設計では、クラスのレイヤー構造が構成されておらず、あらゆるところと関連をもち、設計の全体像をつかみにくい。

■ 解決

オブジェクト指向設計でも、Layer構造(層を重ねる形)でモデリングをする。

「それを持っている」や「それを実現手段とする」の”それ”は、明確に下位レイヤーのオブジェクトとして扱うこと。

■ 補足

標準ライブラリ(STL)を有効に使うことで、効率的に、「それを持っている」、「それを実現手段とする」実装を実現できる。詳細は、Effective C++ 40項をよく読もう!
□ 41項 □ 継承とテンプレートの違いを理解しよう
■ 課題

いつ継承を使ってよいのか、いつテンプレートを使ってよいのかよくわからない。

■ 解決

継承を用いるのは、「その一種である」かどうかが重要になる。一方でテンプレートは、「種類にかかわらず処理する」場合に有効。

・ 入力するオブジェクトの型の違いによって、クラスの関数の振る舞いが変わることがないのであれば、テンプレートを用いる。

・ 入力するオブジェクトの型の違いによって、クラスの関数の振る舞いが変わるのであれば、継承を用いる。

■ 補足

テンプレートは、あまり乱用するとコードの膨張を招く。

基本的には、インライン関数と似たようなものでヘッダーファイルに記述され、定義されたクラスごとにコードが展開されるためのである。

しかし、うまく使えば効率的なプログラミングを実現でき、逆にコードサイズを減らすともできる高度なテクニックを用いることもできる。テンプレートの高度な活用方法については、「Modern C++ Design」に詳しい。

35項
も参照
□ 42項 □ private な継承はよく考えてから使おう
■ 課題

private 継承の意味がよくわからない。以下のプログラムが inaccesible とコンパイラから文句を言われコンパイルできない。

#include <iostream>
using namespace std;

class Base {
public:
__Base() {}
__virtual ~Base() {}
__virtual void f1() { cout << "Base::f1" << endl; }
__void f2() { cout << "Base::f2" << endl; }
};

class Derrived : private Base {
public:
__Derrived() {}
__virtual ~Derrived() {}
__void f3() { cout << "Derrived::f3" << endl; }
};

int main() {
__Derrived* d = new Derrived;
__d->f1(); // コンパイルエラー!
__d->f2(); // コンパイルエラー!
__d->f3();
__delete d;
}

■ 解決

private 継承元の関数へのアクセスは明示的に指定してやらないとアクセスすることができない。

private 継承は、「それを実装手段とする」の意味で、インターフェースを継承するものではない。したがって、private 継承したクラスにキャストしてはいけない。(static_cast でキャストできてしまうこともあるが、基本的に動作は保障できるものではない)。

以下、上記プログラムを修正したもの。

#include <iostream>
using namespace std;

class Base {
public:
__Base() {}
__virtual ~Base() {}
__virtual void f1() { cout << "Base::f1" << endl; }
__void f2() { cout << "Base::f2" << endl; }
};

class Derrived : private Base {
public:
__Derrived() {}
__~Derrived() {}
__void f1() { Base::f1(); } // 明示的にf1を呼ばなくてはならない
__void f2() { Base::f2(); } // 明示的にf2を呼ばなくてはならない
__void f3() { cout << "Derrived::f3" << endl; }
};

int main() {
__Derrived* d = new Derrived;
__d->f1();
__d->f2();
__d->f3();
__delete d;
}

■ 補足

private 継承を使う場面はほとんどない。実際に今まで使っているところを見たことがない。ほとんど場合、コンポジションで代用可能なためである。

コンポジションを用いたほうが柔軟な設計となる。

Effective C++ 42項では、テンプレートで用いることによる功罪も記述しているので、参照してみてください。
□ 43項 □ 多重継承は慎重に使おう
■ 課題

以下のようなダイヤモンド継承すると、派生クラスの経路によって、Baseクラスが異なる値を保持してしまう。

#include <iostream>
using namespace std;

class Base {
public:
__Base(): id(0) {}
__virtual ~Base() {}
__int GetBaseId() { return id; }
__void SetBaseId(int n) { id = n; }
private:
__int id;
};

class DerrivedA : public Base {
public:
__DerrivedA() {}
__virtual ~DerrivedA() {}
__int A_GetBaseId() { return GetBaseId(); }
__void A_SetBaseId(int n) { SetBaseId(n); }
};

class DerrivedB : public Base {
public:
__DerrivedB() {}
__virtual ~DerrivedB() {}
__int B_GetBaseId() { return GetBaseId(); }
__void B_SetBaseId(int n) { SetBaseId(n); }
};

class MultiDerrived : public DerrivedA, public DerrivedB {
public:
__MultiDerrived() {}
__virtual ~MultiDerrived() {}
};

int main() {
__MultiDerrived md;
__md.A_SetBaseId(100);
__md.B_SetBaseId(50);
__// BaseId は同じはず
__cout << md.A_GetBaseId() << endl; // あれ?
__cout << md.B_GetBaseId() << endl;
}


■ 解決

ダイヤモンド継承では、基底クラスの継承に virtual 継承を用いること

ダイヤモンド継承では、DerrivedA + Base、DerrivedB + Base と構成されてしまい、2重にBaseクラスを作ってしまう。それを防ぐには、virtual 継承を用いる。

.... (snip) ....
class DerrivedA :
virtual public Base {
public:
__DerrivedA() {}
__virtual ~DerrivedA() {}
__int A_GetBaseId() { return GetBaseId(); }
__void A_SetBaseId(int n) { SetBaseId(n); }
};

class DerrivedB :
virtual public Base {
public:
__DerrivedB() {}
__virtual ~DerrivedB() {}
__int B_GetBaseId() { return GetBaseId(); }
__void B_SetBaseId(int n) { SetBaseId(n); }
};
.... (snip) ....

■ 補足

ダイヤモンド継承はコンストラクタの初期化にも影響する。たとえば次の例のように、コンストラクタの初期化に race condition が発生 し、階層の深いもののほうが優先されてしまう問題が発生する。

#include <iostream>
using namespace std;

class Base {
public:
__Base(): id(0) {}
__Base(int n): id(n) {}
__virtual ~Base() {}
__int GetBaseId() { return id; }
private:
__int id;
};

class DerrivedA : virtual public Base {
public:
__DerrivedA(int n) : Base(n) {}
__virtual ~DerrivedA() {}
};

class DerrivedB : virtual public Base {
public:
__DerrivedB() {}
__virtual ~DerrivedB() {}
};

class DerrivedC : public DerrivedB {
public:
__DerrivedC() {}
__virtual ~DerrivedC() {}
__int GetId() { return GetBaseId(); }
};

class MultiDerrived : public DerrivedA, public DerrivedC { 
public:
__MultiDerrived(int n) : DerrivedA(n) {}
__virtual ~MultiDerrived() {}
};

int main() {
__MultiDerrived md(10); // id を10の初期化のはずが・・・
__cout << md.GetId() << endl; // あれ?
}

その他、次の例では仮想関数 func は、Base::func が呼ばれるのか、DerrivedA::func が呼ばれるのか、よく考えないとわからない。この場合は、共通のBase::foo をDerrivedA::foo がオーバーライドしているので、DerrivedA::func が呼ばれる。

しかし、virtual 継承をはずすと、Base::foo と オーバーライドしている DerrviceA::foo の2つの参照先が発生するため、曖昧さによってコンパイルエラーとなる。

#include <iostream>
using namespace std;

class Base {
public:
__Base() {}
__virtual ~Base() {}
__virtual void foo() { cout << "Base::foo" << endl; }
};

class DerrivedA : virtual public Base {
public:
__DerrivedA() {}
__virtual ~DerrivedA() {}
__virtual void foo() { cout << "DerrivedA::foo" << endl; }
};

class DerrivedB : virtual public Base {
public:
__DerrivedB() {}
__virtual ~DerrivedB() {}
};

class MultiDerrived : public DerrivedA, public DerrivedB {
public:
__MultiDerrived() {}
__virtual ~MultiDerrived() {}
};

int main() {
__MultiDerrived md;
__md.foo(); // Base::foo ? DerrivedA::foo ?
}

以上のように、ダイヤモンド継承は、virtual 継承にしたとしても、ろくなことがないため、できる限り多重継承は使わずに、コンポジションで対応することを検討すること。
□ 44項 □ あなたの意図することを言おう、自分が言っていることの意味を理解しよう
■ 課題

C++でソフトウェア設計をするために知っておくべきこと。

■ 解決

自分の意図することをソフトウェアの設計に反映する。そして、自分の設計がサブクラスに対してどのような影響を与えるかの理解を深めめクラス設計に反映すること。以下ポイント(本章のまとめ)

・ 共通の基底クラスは共通の特徴を意味する

・ public継承は、「その一種である」という意味がある

・ private継承は、「それを実装手段とする」という意味がある

・ 「それを持っている」、「それを実装手段とする」という関係は、レイヤー構造を導入する

・ 純粋仮想関数は、関数のインターフェースだけが継承される

・ 一般仮想関数は、、その関数のインターフェースとデフォルトの実装が継承される

・ 非仮想関数は、その関数のインターフェースと強制的な実装が継承される

■ 補足

よくわからなかったら、Effective C++ の第六章をよく読もう。

Copyright(C) 2005 Yoshinori Oota All rights reserved.

本ホームページのリンクは自由です。複製、転載される場合は、必ず著者までご連絡をください。