多重継承 | Programming Place Plus C++編【言語解説】 第30章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 と更新されており、今後も 3年ごとに更新されます。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要

この章の概要です。


多重継承

これまでの章では、1つのクラスから継承を行って、派生クラスを定義していましたが、2つ以上のクラスから継承することも可能です。このような、複数のクラスから継承を行うことを、多重継承と呼びます。また、1つのクラスからの継承を、多重継承と対比させて、単一継承と呼びます。

多重継承を行うと、複数ある基底クラスのそれぞれのメンバが、派生クラスに引き継がれます。

多重継承の構文は、以下のようになります。

class クラス名 : アクセス指定子 基底クラス名, アクセス指定子 基底クラス名 {};

このように、「,」で区切って、複数の基底クラスを指定します。それぞれの継承に指定するアクセス指定子は、異なる種類のものが混在しても問題ありません。

class Base1 {};
class Base2 {};
class Derived : public Base1, private Base2 {};

この場合、Base1 を「公開継承」、Base2 を「非公開継承」します。

多重継承のクラス構造

多重継承を使って、次のような構造を作ると、厄介な問題を引き起こすことがあります。

class Ancestor {};
class Base1 : public Ancestor {};
class Base2 : public Ancestor {};
class Derived : public Base1, public Base2 {};

つまり、多重継承の基底になる2つのクラス(3つ以上でも同様)が、1つの共通クラスから派生している構造です。この継承構造を図にすると、次のようになります。

多重継承

ポイントは、Ancestor が2カ所にあることです。

このような構造になるということは、Derivedクラスをインスタンス化すると、Ancestor は2つできるということであり、ここに問題が潜んでいます。つまり、Derivedクラスのオブジェクトから、Ancestorクラスのメンバへアクセスしようとすると、「どの Ancestor なのか?」が曖昧になってしまう訳です。

どのような形で曖昧さが問題になるのか、見ていきましょう。まず、Ancestorクラスにメンバ関数がある場合を考えてみます。

class Ancestor {
public:
    void f1() {}
    virtual void f2() {}
};

class Base1 : public Ancestor {
public:
    virtual void f2() {}
};
class Base2 : public Ancestor {};
class Derived : public Base1, public Base2 {};

int main()
{
    Derived* d = new Derived();
    d->f1();
    d->f2();
    delete d;
}

このプログラムはコンパイルに失敗します。問題なのは、f1、f2 の呼び出しがともに曖昧であることです。 f1 は Ancestorクラスでしか定義されていませんが、Derived から見ると、Base1 の基底としての Ancestor と、Base2 の基底としての Ancestor があるため、どちらの f1 を呼び出そうとしているのか判断できません。

曖昧さを無くすには、Ancestorクラスのメンバへのアクセスが、Base1経由で行うべきものなのか、Base2経由で行うべきものなのかを明示する必要があります。そのための方法としては、以下の2つがあります。

static_cast<Base1*>(d)->f1();
d->Base2::f1();

1つは、キャストを使って、ポインタ(あるいは参照)を基底クラスを指すように変換することです。この方法で解決できるということは、いったん、基底クラスのポインタ型(あるいは参照型)の変数を経由させるのでも構いません。基底クラス型への変換は暗黙的に行えるので、この方法ならば static_cast も不要です。

もう1つの方法は、スコープ解決演算子 :: を使って、基底クラスの名前を明示することです。やや見慣れない構文ですが、これも有効です。

f1 に関していえば、Ancestorクラスでしか定義されていないので、曖昧さの解決を求められることは少々不服に感じますが、f2 は仮想関数であり、Base2 だけでオーバーライドされていますから、曖昧さの解決は重大な意味を持ちます。

今度は、Ancestorクラスがメンバ変数を持っている場合を考えてみましょう。

静的でないメンバ変数は、オブジェクト1つごとにメモリ領域を確保しなければなりませんから、Ancestor を2つ含む形になると、メモリも2カ所に確保されることになります。メモリの使用量が増えることが問題というわけではなく、Base1経由で見たメンバ変数と、Base2経由で見たメンバ変数が別物であるという点が問題になり得ます。

staticメンバ変数の場合は、クラスごとに1つのメモリ領域を取るので、重複することはありません。

確認してみましょう。

#include <iostream>

class Ancestor {
public:
    Ancestor() : mValue(0)
    {}

    inline void Print() const
    {
        std::cout << mValue << std::endl;
    }

protected:
    inline void SetValue(int value)
    {
        mValue = value;
    }

private:
    int mValue;
};

class Base1 : public Ancestor {
public:
    inline void Set(int value)
    {
        SetValue(value);
    }
};

class Base2 : public Ancestor {
public:
    inline void Set(int value)
    {
        SetValue(value);
    }
};

class Derived : public Base1, public Base2 {};

int main()
{
    Derived* d = new Derived();
    d->Base1::Set(100);
    d->Base2::Set(200);
    d->Base1::Print();
    d->Base2::Print();
    delete d;
}

実行結果:

100
200

Base1 経由で設定した値と、Base2 経由で設定した値とが、別物として扱われていることが分かると思います。


仮想継承

多重継承の際に、共通の基底クラスを、本当にただ1つの実体として持ちたい場合には、仮想継承を用います。仮想継承を行うには、基底クラスを指定する際に virtual指定子を付加します。

class Ancestor {};
class Base1 : public virtual Ancestor {};
class Base2 : public virtual Ancestor {};
class Derived : public Base1, public Base2 {};

こうすると、継承構造は次の図のように、菱形(ダイアモンド形)になります。

多重継承

このように、Base1 経由でみた Ancestor と、Base2 経由でみた Ancestor は同一のものになりました。なおこのとき、Ancestor は、仮想基底クラスと呼ばれます。前の項で試したプログラムを、仮想継承に変更して再度実行して確認してみます。

#include <iostream>

class Ancestor {
public:
    Ancestor() : mValue(0)
    {}

    inline void Print() const
    {
        std::cout << mValue << std::endl;
    }

protected:
    inline void SetValue(int value)
    {
        mValue = value;
    }

private:
    int mValue;
};

class Base1 : public virtual Ancestor {
public:
    inline void Set(int value)
    {
        SetValue(value);
    }
};

class Base2 : public virtual Ancestor {
public:
    inline void Set(int value)
    {
        SetValue(value);
    }
};

class Derived : public Base1, public Base2 {};

int main()
{
    Derived* d = new Derived();
    d->Base1::Set(100);
    d->Base2::Set(200);
    d->Base1::Print();
    d->Base2::Print();
    delete d;
}

実行結果:

200
200

後から設定した方の値 200 しか、出力されていないことが分かります。

ところで、Ancestor が1つになったということは、Printメンバ関数の呼び出しについては、曖昧さが無くなったはずです。実際、次のように呼び出すことができます。

d->Print();


練習問題

問題① 「飛べるもの」を表す抽象クラス FlyObject と、「乗り物」を表す抽象クラス Vehicle があるとき、「飛行機」のクラスを多重継承を使って定義してください。FlyObject と Vehicle はそれぞれ、速度を表すメンバ変数 mSpeed と、それを返す GetSpeedメンバ関数を持つものとします。

問題② 問題①において、FlyObjectクラスを廃止して、代わりに「飛べるもの」を表すインターフェース IFly を導入したら、派生クラスはどう変化するでしょうか。IFlyインターフェースは、「飛ぶ」という動作にしか興味がないので、Fly という純粋仮想関数と仮想デストラクタのみを持ちます。


解答ページはこちら

参考リンク


更新履歴

’2018/7/13 サイト全体で表記を統一(「静的メンバ」–>「staticメンバ」)

’2016/5/2 新規作成。



前の章へ (第29章 抽象クラスとインターフェース)

次の章へ (第31章 RTTI)

C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る