継承には3つのレベルがあります。
- public
- protected
- private
オブジェクト指向的な考え方ではpublic継承はよくis-a関係を表すなどと言われたりしますね。
どら焼き is a 和菓子。
ベジータ is a サイヤ人。
B is a A(びーいずあえー)という関係なのでis-a関係です。
一方、private継承、protected継承はis-a関係ではありません。
is-a関係との対比でhas-a関係、またはis-implemented-in-terms-of関係なんて呼ばれたりします。
public継承以外はB is a Aの関係では無いため、AのポインタにBを代入することはできません。
1 2 3 4 5 6 7 |
class A {}; class B : A {}; int main() { A* = new B; // エラー!cannot cast 'B' to its private base class 'A' return 0; } |
基底クラスのポインタへの代入を許すのはpublic継承だけです。
public継承以外の継承では基底クラスのメンバへは内部からしかアクセスできません。ですので、あるクラスをprivate継承していようが、メンバとしてもっていようが、Bというクラスの実装の詳細の為に内部でAの機能を使うという状態は実際のところ変わりません。
つまり、あるクラスの持つ機能を使いたい場合には
- メンバとして持つ
- 継承する
という2つの選択肢があります。
基本的に継承はコードの複雑性を増すので避けられるなら避けた方が望ましいとされています。できるだけコンポジション(内包)を使って実装する方が良いでしょう。
それでも継承を使って実装するシチュエーションはなんでしょうか?
1つには、EBO(Empty Base Optimization)が効くケースがあるという点が挙げられます。これはメモリ効率の観点からすると大きなアドバンテージとなります。
2つ目は、あなたがライブラリの設計者で、自作するクラスBは、Aというクラスの仮想関数をオーバーライドして何かしたいけど、ユーザーには自分の作ったクラスをAのポインタとして管理させたくない。という場合です。何を言っているのでしょうか。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
#include <iostream> // もともとあったクラスA class A { public: void do3() { // do()を3回呼ぶメソッド do(); do(); do(); } // この関数をオーバーライドして好きな挙動にすればいいさ virtual void do() { std::cout << "A-do" << std::endl; } }; // クラスAの実装を一部カスタマイズして実装したいB class B : A { public: void do3() { A::do3(); } private: // doの新しい挙動はこれだ! void do() override { std::cout << "B-do" << std::endl; } }; int main() { B b; b.do3(); return 0; } |
これで画面には”B-do”と3回表示されます。
あまり実用性を感じない例ですが、あるクラスの一部の挙動をオーバーライドでカスタマイズしたい場合は継承するしかありません。このようなケースは本来ならpublic継承でいいのですが、なんらかの理由(基底クラスのデストラクタがvirtualになっていない等)で基底クラスのポインタによる管理を許さない設計にしたい場合は前述のとおり、public継承しないことで実現できます。
STLのコンテナは全てデストラクタがvirtualではありません。ですのでこれらのクラスを拡張した便利コンテナクラスを作成したい場合も絶対にpublic継承してはいけません。とはいえ、例えばstd::vectorの拡張クラスを作りたい時に、std::vectorをメンバとして持つと、std::vectorの持つメソッドを全て再実装しなければいけないという手間が発生します。
上記のようなケースでは継承とusingを組み合わせることで実装がシンプルになります。
1 2 3 4 5 6 7 8 9 10 11 12 |
template <typename T> class MyVector : private std::vector<T, std::allocator<T>> { // STLコンテナは絶対public継承してはダメです public: using std::vector<T>::push_back; // usingでpush_backのアクセスレベルをpublicに引き上げる }; int main() { MyVector<int> v; v.push_back(99); // private継承したvectorのpush_backにアクセスできる return 0; } |
boost::noncopyableはデータを持たないクラスなのでEBOが効き、さらにデストラクタがvirtualではないので、private継承にうってつけの例と言えます。
まとめ
- public継承以外は基底クラスのポインタへの代入はできない
- デストラクタがvirtualではないクラスは絶対public継承してはいけない
- 継承ではなく内包で実装できる時はなるべくそうしたほうが良い
- 非public継承すべき時は特殊だが存在するので、c++の挙動を正しく理解して判断する