複数の基本クラス

クラスは、複数の基底クラスから派生させることができます。 多重継承モデル (クラスが複数の基底クラスから派生される) の場合、基底クラスは base-list 文法要素を使用して指定されます。 たとえば、CollectionOfBook および Collection から派生する Book のクラス宣言は指定できます。

// deriv_MultipleBaseClasses.cpp
// compile with: /LD
class Collection {
};
class Book {};
class CollectionOfBook : public Book, public Collection {
    // New members
};

基底クラスを指定する順序は、コンストラクターとデストラクターが呼び出されている特定の場合を除き、重要ではありません。 このような場合は、基底クラスを指定する順序は次に影響します。

  • コンストラクターで初期化が行われる順序。 コードが、Book パーツの前で初期化されるために、CollectionOfBookCollection 部分に依存している場合、指定の順序が重要になります。 初期化は、base-list に指定されているクラスの順序で実行されます。

  • デストラクターがクリーンアップされるために呼び出される順序。 ここでも、他のパーツの破棄時にクラスの特定の「パーツ」が存在する必要がある場合、順序が重要になります。 デストラクターは、base-list に指定されているクラスの逆の順序で呼び出されます。

    Note

    基底クラスの指定の順序はクラスのメモリ レイアウトに影響を与える場合があります。 メモリの基本メンバーの順序に基づいて、プログラムの決定を行わないでください。

base-list を指定するときは、同じクラス名を複数回指定できません。 ただし、間接基底クラスは複数回派生クラスとなることができます。

仮想基底クラス

クラスは派生クラスへの間接基底クラスであることが複数回可能であるため、C++ にはこのような基底クラスの動作を最適化する方法が用意されています。 仮想基底クラスは、領域を節約し、多重継承を使用するクラス階層でのあいまいさを避ける方法を提供します。

非仮想オブジェクトはそれぞれ、基底クラスで定義されたデータ メンバーのコピーを含んでいます。 この重複によって領域が浪費され、基底クラスのメンバーのコピーにアクセスするたびに、どちらのコピーかを指定しなければならなくなります。

仮想基底クラスとして指定された基底クラスは、データ メンバーを複製しなくても、間接基底クラスとして複数回使用できます。 データ メンバーの 1 つのコピーが、仮想基底クラスとして使用するすべての基底クラスで共有されます。

仮想基底クラスを宣言すると、virtual キーワードが派生クラスの基底クラスのリストに表示されます。

昼食の行列をシミュレートした、次の図のクラス階層構造を考えます。

Graph of simulated lunch line.
昼食の行列シミュレーションのグラフ

図で、Queue は、CashierQueue および LunchQueue の基底クラスです。 ただし、LunchCashierQueue を作成するために両方のクラスを組み合わせると、新しいクラスに、Queue 型のサブオブジェクトが 2 つ (1 つは CashierQueue のサブオブジェクト、もう 1 つは LunchQueue のサブオブジェクト) が含まれるという問題が生じます。 次の図は、概念的なメモリ レイアウトを示します (実際のメモリ レイアウトは最適化される場合があります)。

Simulated lunch line object.
Lunch-Line シミュレーション オブジェクト

Queue オブジェクトに 2 つの LunchCashierQueue サブオブジェクトがあることに注意してください。 次のコードは、Queue が仮想基底クラスであることを宣言します。

// deriv_VirtualBaseClasses.cpp
// compile with: /LD
class Queue {};
class CashierQueue : virtual public Queue {};
class LunchQueue : virtual public Queue {};
class LunchCashierQueue : public LunchQueue, public CashierQueue {};

virtual キーワードにより、サブオブジェクト Queue のコピーが 1 つだけ含まれるようになります (次の図を参照)。

Diagram showing a simulated lunch line object, with virtual base classes.
仮想基底クラスを持つ、昼食の行列シミュレーションのオブジェクト

クラスは、指定された型の仮想コンポーネントと非仮想コンポーネントの両方を持つことができます。 これは、次の図に示されている条件で発生します。

Diagram showing virtual and non virtual components of a class.
同一クラスの仮想および非仮想コンポーネント

図では、CashierQueueLunchQueue は仮想基底クラスとして Queue を使用します。 ただし、TakeoutQueue は、仮想基底クラスではなく、基底クラスとして Queue を指定します。 したがって、LunchTakeoutCashierQueue には型 Queue の 2 つのサブオブジェクトがあります。1 つは LunchCashierQueue を含む継承パスからのもので、もう 1 つは TakeoutQueue を含むパスからのものです。 これを次の図に示します。

Diagram showing virtual and non virtual inheritance in object layout.
仮想および非仮想継承によるオブジェクトのレイアウト

Note

仮想継承は、非仮想継承と比較してサイズに関して大きな利点があります。 ただし、余分な処理オーバーヘッドが生じる場合があります。

派生クラスが仮想基底クラスから継承する仮想関数をオーバーライドする場合、および派生基底クラスのコンストラクターまたはデストラクターが仮想基底クラスへのポインターを使用してその関数を呼び出す場合、コンパイラは仮想基底クラスを含むクラスに追加の vtordisp 隠しフィールドを導入する場合があります。 /vd0 コンパイラ オプションは、隠された vtordisp コンストラクター/デストラクター ディスプレイスメント メンバーの追加を抑制します。 /vd1 コンパイラ オプション (既定) は、これらを必要に応じて有効にします。 すべてのクラス コンストラクターとデストラクターが仮想的に仮想関数を呼び出すことが確実な場合にだけ、vtordisp をオフにしてください。

/vd コンパイラ オプションは、コンパイル モジュール全体に影響します。 vtordisp pragma を使用すると、vtordisp フィールドをクラス単位で無効化したり、再度有効化したりできます。

#pragma vtordisp( off )
class GetReal : virtual public { ... };
\#pragma vtordisp( on )

名前のあいまいさ

多重継承には、名前が、複数のパスに沿って継承される可能性があります。 これらのパスに沿ったクラス メンバーの名前は、必ずしも一意ではありません。 これらの名前の競合は "あいまいさ" と呼ばれます。

クラス メンバーを参照する式は、明確な参照を作成する必要があります。 次の例は、あいまいさがどのように増すかを示します。

// deriv_NameAmbiguities.cpp
// compile with: /LD
// Declare two base classes, A and B.
class A {
public:
    unsigned a;
    unsigned b();
};

class B {
public:
    unsigned a();  // Note that class A also has a member "a"
    int b();       //  and a member "b".
    char c;
};

// Define class C as derived from A and B.
class C : public A, public B {};

前のクラス宣言では、bb または A のどちらの B を参照しているか不明確であるため、次のようなコードはあいまいになります。

C *pc = new C;

pc->b();

前の例を考えます。 名前 aA クラスと B クラスの両方のメンバーであるため、コンパイラはどの a が呼び出される関数を指定するか識別できません。 メンバーへのアクセスは、複数の関数、オブジェクト、型、または列挙子を参照できる場合はあいまいになります。

コンパイラは、この順序でテストを実行することにより、あいまいさを検出します。

  1. 名前へのアクセスが (単に記述されているとおりに) あいまいな場合、エラー メッセージが生成されます。

  2. オーバーロードされた関数があいまいでなければ、その関数は解決されます

  3. 名前へのアクセスがメンバーのアクセス許可に違反する場合、エラー メッセージが生成されます (詳細については、「メンバー アクセス コントロール」を参照)。

式が継承によるあいまいさを生成するときは、クラス名で該当する名前を修飾することにより手動で解決できます。 前の例をあいまいさなしで正しくコンパイルするには、コードを次のように使用します。

C *pc = new C;

pc->B::a();

Note

C を宣言すると、BC のスコープ内で参照されるとエラーが発生することがあります。 ただし、B のスコープ内で C への不適切な参照が実際に行われるまで、エラーは発行されません。

優先度

継承グラフをたどったときに複数の名前 (関数、オブジェクト、または列挙子) に到達することがあります。 そのようなケースは、非仮想基底クラスではあいまいであると見なされます。 名前のいずれかの "優先度" が他よりも高くない限り、仮想基底クラスでもあいまいになります。

1 つのクラスから別のクラスが派生していて、その両方に同じ名前が定義されている場合、一方の名前の優先度がもう一方より高くなります。 優先度が高くなる名前は、派生クラスにある名前です。この名前は、これを使用しないとあいまいさが発生する可能性がある場合に使用されます。次の例を参照してください。

// deriv_Dominance.cpp
// compile with: /LD
class A {
public:
    int a;
};

class B : public virtual A {
public:
    int a();
};

class C : public virtual A {};

class D : public B, public C {
public:
    D() { a(); } // Not ambiguous. B::a() dominates A::a.
};

あいまいな変換

クラス型へのポインターまたは参照からの明示的および暗黙的な変換によって、あいまいさが生じる可能性があります。 次の図、「基底クラスへのポインターのあいまいな変換」は、以下のことを示しています。

  • D 型のオブジェクトの宣言。

  • このオブジェクトにアドレス演算子 (&) を適用する効果。 アドレス演算子は、常にオブジェクトのベース アドレスを指定することに注意してください。

  • アドレス演算子を使用して取得したポインターを、基底クラス型 A に明示的に変換する効果。 オブジェクトのアドレスを A* 型に強制変換しても、2 つのサブオブジェクトが存在する場合、A 型のどのサブオブジェクトを選択するのかについて十分な情報が必ずコンパイラに提供されるわけではありません。

Diagram showing ambiguous conversion of pointers to base classes.
基底クラスへのポインターのあいまいな変換

A* 型のサブオブジェクトのどれが正しいかを識別する方法はないため、A 型への変換 (A へのポインター) はあいまいです。 次のように、使用するサブオブジェクトを明示的に指定することにより、あいまいさを避けることができます。

(A *)(B *)&d       // Use B subobject.
(A *)(C *)&d       // Use C subobject.

あいまいさと仮想基底クラス

仮想基底クラスが使用されている場合、関数、オブジェクト、型、および列挙子には、多重継承のパスを通じて到達できます。 基底クラスのインスタンスは 1 つだけあるため、これらの名前にアクセスする場合にあいまいさはありません。

次の図は、仮想継承と非仮想継承を使用してオブジェクトがどのように構成されているかを示しています。

Diagram showing virtual derivation and nonvirtual derivation.
仮想派生と非仮想派生

この図では、非仮想基底クラスを通じてクラス A のメンバーにアクセスすると、あいまいさが発生します。コンパイラは、B に関連付けられているサブオブジェクトと C に関連付けられているサブジェクトのどちらを使用するかを示す情報を持ちません。 しかし、A が仮想基底クラスとして指定されている場合、どのサブオブジェクトがアクセスされているかは問題になりません。

関連項目

継承