多重継承
今までの継承は、すべて基底クラスがひとつだけの継承で説明してきました。
これを単一継承といい、よく行われる継承です。
これに対して、基底クラスを複数指定して継承を行うこともできます。
これを多重継承と言います。
シンプルな多重継承のコード
#include <iostream>
class BaseA
{
public:
int num = 0;
};
class BaseB
{
public:
int arr[3] = { 1,2,3 };
};
class DerivedClass : public BaseA, private BaseB
{
public:
void print()
{
std::cout << num << std::endl;
std::cout << arr[0] << std::endl;
}
};
サンプルコードのDerivedClass
クラスは、BaseA
クラスとBaseB
クラスのふたつのクラスを基底クラスとする派生クラスです。
その中身は当然、それぞれの基底クラスを合わせた内容となります。
多重継承の文法は、引数の複数指定のようにコンマで区切るだけです。
その際、継承のアクセス指定子(public
、protected
、private
)はそれぞれで異なっても構いません。
必要ならば、基底クラスを三つ、四つと増やすこともできます。
多重継承の注意点
継承関係の複雑化
多重継承は、ただでさえ複雑になりがちな継承の中でも特に複雑になります。
そのため、クラス設計をしっかりとして全体を把握しないと自分でもよくわからないコードとなり、バグを招きます。
不用意に多重継承を行うのは避けたほうが良いでしょう。
ただしインターフェイスクラスを含む多重継承はそれなりに実用性が高いです。
インターフェイスクラスには実装がなく、派生クラス側ですべてオーバーライドにより定義する必要があります。
基底クラスへの依存度が少なく、オーバーライドのし忘れによるバグもありません。
クラスを使う側からはそのクラスが「できること」が明示されるので、使いやすさも上がります。
名前の衝突
複数のクラスを基底クラスとするので、それぞれの基底クラスに同じ名前のメンバが存在すると、名前の衝突が起こります。
#include <iostream>
class BaseA
{
public:
int num;
void func() {}
};
class BaseB
{
public:
int num;
void func() {}
};
class DerivedClass : public BaseA, public BaseB
{
void a()
{
//エラー: 名前が曖昧
//num = 0;
//func();
}
};
これはスコープ解決演算子(::
)でそれぞれの基底クラスを指定してアクセスできます。
#include <iostream>
class BaseA
{
public:
int num;
void func() {}
};
class BaseB
{
public:
int num;
void func() {}
};
class DerivedClass : public BaseA, public BaseB
{
public:
void print()
{
BaseA::num = 0;
BaseB::func();
}
};
int main()
{
DerivedClass dc;
dc.BaseA::num = 1;
dc.BaseB::func();
}
ダイヤモンド継承(菱形継承)
C++では、派生クラスからさらに派生クラスを作ることができます。
派生クラスの派生クラスを、ここでは孫クラスと呼ぶことにします。
(親クラス(基底クラス)、子クラス(派生クラス)という呼び方から)
継承の継承を用いれば、以下のようにある基底クラスを継承するクラスを二つ作り、さらにそれら二つのクラスを多重継承した孫クラスを作ることが可能です。
class Base
{
};
class DerivedA : public Base
{
};
class DerivedB : public Base
{
};
//派生クラスを多重継承
class DerivedX : public DerivedA, public DerivedB
{
};
これを図で表すとこのようになります。
このような形になることから、これはダイヤモンド継承(菱形継承)と呼ばれます。
ダイヤモンド継承にはいくつかの注意点があります。
基底クラスの扱い
以下はメンバ変数とメンバ関数がそれぞれひとつだけの単純なクラスをダイヤモンド継承した例です。
派生クラスではメンバは定義せず、そのまま継承しただけです。
#include <iostream>
class Base
{
private:
int num;
public:
Base(int n) : num(n) {}
void print()
{
std::cout << num << std::endl;
}
};
class DerivedA : public Base
{
public:
DerivedA(int n) : Base(n) {}
};
class DerivedB : public Base
{
public:
DerivedB(int n) : Base(n) {}
};
class DerivedX : public DerivedA, public DerivedB
{
public:
DerivedX(int n) : DerivedA(n), DerivedB(n) {}
};
int main()
{
DerivedX dx(10);
//エラー
dx.print();
std::cin.get();
}
孫クラスDerivedX
のインスタンスからメンバ関数print
を呼び出そうとしてもエラーになります。
メンバ関数print
の呼び出しはBase
クラスから直接呼び出せず、DerivedA
かDerivedB
かのどちらかを通して呼び出さねばなりません。
そのため、関数名だけではどちらを経由して呼び出して良いのかが曖昧になるのです。
これはスコープ解決演算子を使用することで解決できます。
それもあわせて、上のコードを少しだけ変更してみます。
#include <iostream>
class Base
{
private:
int num;
public:
Base(int n) : num(n) {}
void print()
{
std::cout << "Base: " << num << std::endl;
}
};
class DerivedA : public Base
{
public:
DerivedA(int n) : Base(n) {}
};
class DerivedB : public Base
{
public:
DerivedB(int n) : Base(n) {}
};
class DerivedX : public DerivedA, public DerivedB
{
public:
//引数を変更
DerivedX(int x, int y) : DerivedA(x), DerivedB(y) {}
};
int main()
{
DerivedX dx(10, 20);
//スコープ解決演算子を使用
dx.DerivedA::print();
dx.DerivedB::print();
std::cin.get();
}
孫クラスDerivedX
のコンストラクタの引数を二つに増やし、DerivedA
とDerivedB
にそれぞれ別の引数を与えています。
Base
クラス、DerivedA
、DerivedB
クラスの定義は最初と同じです。
このコードの実行結果は以下となります。
Base: 10 Base: 20
print
関数をDerivedA
から呼び出した時とDerivedB
から呼び出したときとでは値が異なります。
つまり、C++でダイヤモンド継承をすると基底クラスの実体はふたつ存在することになります。
図にすると以下のようになります。
DerivedA
が継承したAクラスと、DerivedB
が継承したBase
クラスとではそれぞれ実体が異なり、メンバは別物として扱われています。
以下のようにBase
クラスにセッターを用意して値を書き換えてみると良くわかるでしょう。
#include <iostream>
class Base
{
private:
int num;
public:
void set(int n) { num = n; }
void print()
{
std::cout << "Base: " << num << std::endl;
}
};
class DerivedA : public Base
{
};
class DerivedB : public Base
{
};
class DerivedX : public DerivedA, public DerivedB
{
};
int main()
{
DerivedX dx;
dx.DerivedA::set(10);
dx.DerivedB::set(20);
dx.DerivedA::print();
dx.DerivedB::print();
std::cin.get();
}
実行結果は同じです。
Base: 10 Base: 20
仮想継承
C++では仮想継承という方法を用いると、基底クラスの実体をひとつにすることができます。
#include <iostream>
class Base
{
private:
int num;
public:
void set(int n) { num = n; }
void print()
{
std::cout << "Base: " << num << std::endl;
}
};
//仮想継承
class DerivedA : public virtual Base
{
};
//仮想継承
class DerivedB : public virtual Base
{
};
//多重継承
class DerivedX : public DerivedA, public DerivedB
{
};
int main()
{
DerivedX dx;
dx.DerivedA::set(10);
dx.DerivedB::set(20);
dx.DerivedA::print();
dx.DerivedB::print();
std::cin.get();
}
DerivedA
とDerivedB
の継承時に、virtual
というキーワードを付けただけで、後は同じです。
実行結果は以下になります。
Base: 20 Base: 20
print
関数をDerivedA
から呼び出しても、DerivedB
から呼び出しても同じ値となります。
これはセッター(set
関数)でも同様で、どちらを通して値を書き換えても結局同じ実体の値を書き換えていることになります。
これは最初のイメージ図通りの動作と言えます。
仮想継承時のコンストラクタと基底クラスのメンバへのアクセス
仮想継承を用いると基底クラスの実体はひとつになりますから、曖昧さがなくなります。
そのため、孫クラスのインスタンスから基底クラスのメンバに直接アクセスが可能となります。
#include <iostream>
class Base
{
private:
int num;
public:
Base(int n) : num(n)
{
std::cout << "Base" << std::endl;
}
void print()
{
std::cout << "Base: " << num << std::endl;
}
};
//仮想継承
class DerivedA : public virtual Base
{
public:
DerivedA() : Base(1)
{
std::cout << "DerivedA" << std::endl;
}
};
//仮想継承
class DerivedB : public virtual Base
{
public:
DerivedB() : Base(2)
{
std::cout << "DerivedB" << std::endl;
}
};
//多重継承
class DerivedX : public DerivedA, public DerivedB
{
public:
DerivedX(int n) : Base(n) //基底クラスのコンストラクタ呼び出し
{
std::cout << "DerivedX" << std::endl;
}
};
int main()
{
DerivedX dx(10);
std::cout << std::endl;
dx.DerivedA::print();
dx.DerivedB::print();
dx.print(); //Baseクラスのメンバに直接アクセス
std::cin.get();
}
それぞれのクラスのコンストラクタで自分のクラス名を出力するように変更しています。
Base
クラスのコンストラクタ呼び出しには引数が必要です。
(デフォルトコンストラクタがない)
実行結果は以下になります。
Base DerivedA DerivedB DerivedX Base: 10 Base: 10 Base: 10
基底クラスから順にコンストラクタが呼び出されるのは通常の継承と同じです。
孫クラスDerivedX
からは直接基底クラスBase
のコンストラクタを呼び出します。
DerivedA
、DerivedB
のコンストラクタには引数がないので今回は指定していませんが、必要ならばこれらの派生クラスのコンストラクタも孫クラスから呼び出します。
(今回はデフォルトコンストラクタが呼び出されています)
Base
クラスのコンストラクタは一回だけしか呼び出されていないこと、メンバ変数は孫クラスで指定された値になっていることに注目してください。
ダイヤモンド継承で孫クラスのインスタンスを生成する場合、基底クラスのコンストラクタ呼び出しは孫クラスから行われ、派生クラスDerivedA
とDerivedB
で定義している基底クラスのコンストラクタ呼び出しは使われません。
ただし、派生クラスのインスタンスを生成する場合には必要ですから、コンストラクタ呼び出しの記述を省略することはできません。
(デフォルトコンストラクタがあれば省略可能です)
同じように、孫クラスDerivedX
のインスタンスからは、基底クラスBase
のメンバに直接アクセスできます。
スコープ解決演算子を使用すれば、明示的にDerivedA
やDerivedB
で継承されたメンバにアクセスすることもできます。
これはBase
のメンバ関数をオーバーライドし、動作を変更している場合などに意味のある呼び出し方です。