メインコンテンツ

バーチャル継承の欠落

基底クラスが同じ階層内でバーチャルに継承され、また非バーチャルに継承されている

説明

この欠陥は、以下の場合に発生します。

  • あるクラスが複数の基底クラスから派生しており、その基底クラスの一部はそれ自体が共通の基底クラスから派生している。

    たとえば、クラス Final が 2 つのクラス、Intermediate_left および Intermediate_right から派生しており、Intermediate_leftIntermediate_right のいずれも、共通のクラス Base から派生している場合などです。

  • 共通の基底クラスからの継承のうち少なくとも 1 つが virtual であり、少なくとも 1 つが virtual ではない。

    たとえば、Base からの Intermediate_right の継承が virtual であり、Base からの Intermediate_left の継承は virtual ではない場合などです。

リスク

この欠陥が発生すると、基底クラスのデータ メンバーのコピーが、最終的な派生クラス オブジェクトに複数出現します。基底クラスのデータ メンバーの正しいコピーにアクセスするため、最終的な派生クラスでは、メンバーとメソッド名を適正に修飾する必要があります。そのため、開発でエラーが発生しやすくなります。

たとえば、この欠陥が発生すると、基底クラスのデータ メンバーのコピーが、クラス Final のオブジェクトに 2 つ現れます。クラス Final でメソッド名を適正に修飾しない場合、値を Base のデータ メンバーに代入できても、同じ値を取得することはできません。

  • 値の代入には、Intermediate_left 経由でアクセスする Base メソッドを使用します。したがって、値は Base メンバーの一方のコピーに代入されます。

  • 値の取得には、Intermediate_right 経由でアクセスする Base メソッドを使用します。したがって、Base メンバーの別のコピーが取得されることになります。

修正方法

あるクラスが、それ自体が共通の基底クラスから派生している複数の基底クラスから派生している場合、すべての中間継承を virtual として宣言します。

中間派生クラスで表されるような Base データ メンバーのコピーが実際に複数必要である場合は、継承の代わりに集約を使用します。たとえば、Final クラス内でクラス Intermediate_leftIntermediate_right の 2 つのオブジェクトを宣言します。

すべて展開する

#include <stdio.h>
class Base {
public:
    explicit Base(int i): m_b(i) {};
    virtual ~Base() {};
    virtual int get() const {
        return m_b;
    }
    virtual void set(int b) {
        m_b = b;
    }
private:
    int m_b;
};

class Intermediate_left: virtual public Base {
public:
    Intermediate_left():Base(0), m_d1(0) {};
private:
    int m_d1;
};

class Intermediate_right: public Base {
public:
    Intermediate_right():Base(0), m_d2(0) {};
private:
    int m_d2;
};

class Final: public Intermediate_left, Intermediate_right {
public:
    Final(): Base(0), Intermediate_left(), Intermediate_right() {};
    int get() const {
        return Intermediate_left::get();
    }
    void set(int b) {
        Intermediate_right::set(b);
    }
    int get2() const {
        return Intermediate_right::get();
    }
};

int main(int argc, char* argv[]) {
    Final d;
    int val = 12;
    d.set(val);
    int res = d.get();
    printf("d.get=%d\n",res);             // Result: d.get=0
    printf("d.get2=%d\n",d.get2());       // Result: d.get2=12
    return res;
}

この例では、FinalIntermediate_leftIntermediate_right の両方から派生しています。Intermediate_leftBase から非 virtual な方法で派生し、Intermediate_rightBase から virtual な方法で派生します。したがって、最終的な派生クラスには、基底クラスとデータ メンバー m_b のコピーが 2 つ存在します。

Intermediate_leftIntermediate_right の両方の派生クラスが、Base のクラス メソッド get および set をオーバーライドするわけではありません。しかし、Final は両方のメソッドをオーバーライドします。オーバーライドされた get メソッドでは、Base::getIntermediate_left を通して呼び出されます。オーバーライドされた set メソッドでは、Base::setIntermediate_right を通して呼び出されます。

ステートメント d.set(val) に続いて、m_bIntermediate_right のコピーが 12 に設定されます。しかし、m_bIntermediate_left のコピーは依然 0 です。したがって、d.get() が呼び出されると、値 0 が取得されることになります。

printf ステートメントを使用すると、取得される値が設定された値と異なることがわかります。

欠陥は、最終的な派生クラス定義と、共通の基底クラスからバーチャルに派生しているクラスの名前に表示されます。ソース コード内での移動のヒントを以下に示します。

  • クラスの定義を見つけるには、[ソース] ペインでクラス名を右クリックし、[定義に移動] を選択します。

  • クラス階層を上に移動するには、まず中間クラス定義に移動します。中間クラス定義で基底クラス名を右クリックして [定義に移動] を選択します。

修正 — 両方の継承をバーチャルにする

1 つの修正方法として、Base からの継承を両方とも virtual と宣言します。

Final のオーバーライドされたメソッド get および set で、Base::getBase::set が異なるクラスを通して呼び出されることに変わりはありませんが、Final に存在する m_b のコピーは 1 つのみになります。

#include <stdio.h>
class Base {
public:
    explicit Base(int i): m_b(i) {};
    virtual ~Base() {};
    virtual int get() const {
        return m_b;
    }
    virtual void set(int b) {
        m_b = b;
    }
private:
    int m_b;
};

class Intermediate_left: virtual public Base {
public:
    Intermediate_left():Base(0), m_d1(0) {};
private:
    int m_d1;
};

class Intermediate_right: virtual public Base {
public:
    Intermediate_right():Base(0), m_d2(0) {};
private:
    int m_d2;
};

class Final: public Intermediate_left, Intermediate_right {
public:
    Final(): Base(0), Intermediate_left(), Intermediate_right() {};
    int get() const {
        return Intermediate_left::get();
    }
    void set(int b) {
        Intermediate_right::set(b);
    }
    int get2() const {
        return Intermediate_right::get();
    }
};

int main(int argc, char* argv[]) {
    Final d;
    int val = 12;
    d.set(val);
    int res = d.get();
    printf("d.get=%d\n",res);             // Result: d.get=12
    printf("d.get2=%d\n",d.get2());       // Result: d.get2=12
    return res;
}

結果情報

グループ: オブジェクト指向
言語: C++
既定値: オフ
コマンド ライン構文: MISSING_VIRTUAL_INHERITANCE
影響度: Medium

バージョン履歴

R2015b で導入