複数の基本クラス

クラスは、複数の基底クラスから派生させることができます。 多重継承モデル (クラスが複数の基底クラスから派生される) の場合、基底クラスは 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

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

ベース リスト指定する場合、同じクラス名を複数回指定することはできません。 ただし、1 つのクラスが派生クラスの間接基底になる可能性は 2 回以上です。

仮想基底クラス

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

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

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

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

シミュレートされたランチ ラインを示す、次の図のクラス階層について考えてみましょう。

Diagram of a simulated lunch line.

基底クラスは Queue です。 キャッシャー キューとランチ キューはどちらも Queue から継承されます。 最後に、ランチ キャッシャー キューは、キャッシャー キューとランチ キューの両方から継承されます。

シミュレートされたランチライン グラフ

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

Diagram of a simulated lunch line object.

この図は、キャッシャー キューとランチ キューという 2 つのサブオブジェクトが含まれるランチ キャッシャー キュー オブジェクトを示しています。 キャッシャー キューとランチ キューの両方に Queue サブオブジェクトが含まれています。

シミュレートされたランチ ライン オブジェクト

オブジェクトには 2 つの Queue サブオブジェクトがあります 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 of a simulated lunch line object, with virtual base classes depicted.

この図は、キャッシャー キュー サブオブジェクトとランチ キュー サブオブジェクトを含むランチ キャッシャー キュー オブジェクトを示しています。 キャッシャー キューとランチ キューはどちらも同じ Queue サブオブジェクトを共有します。

仮想基底クラスを使用したシミュレートされたランチライン オブジェクト

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

Diagram of virtual and non virtual components of a class.

この図は、キューの基本クラスを示しています。 キャッシャー キュー クラスとランチ キュー クラスは、実質的に Queue から継承されます。 3 番目のクラスである Takeout Queue は、キューから非仮想的に継承します。 ランチ キャッシャー キューは、キャッシャー キューとランチ キューの両方から継承されます。 ランチの引き取りレジ担当者キューは、ランチ キャッシャー キューと引き出しキューの両方から継承されます。

同じクラスの仮想コンポーネントと非仮想コンポーネント

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

Diagram of the object layout for virtual and non virtual inheritance.

2 つのサブオブジェクト (Queue サブオブジェクトを含む) とランチ キャッシャー キューの 2 つのサブオブジェクトを含むランチ引き出しキャッシャー キュー オブジェクトが表示されます。 Lunch Cashier Queue サブオブジェクトには、キャッシャー キュー サブオブジェクトとランチ キュー サブオブジェクトが含まれており、どちらも Queue サブオブジェクトを共有します。

仮想継承と非仮想継承を使用したオブジェクト レイアウト

Note

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

派生クラスが仮想基底クラスから継承する仮想関数をオーバーライドし、派生基底クラスのコンストラクターまたはデストラクターが仮想基底クラスへのポインターを使用してその関数を呼び出す場合、コンパイラは仮想基底を持つクラスに他の非表示の "vtordisp" フィールドを導入する可能性があります。 /vd0 コンパイラ オプションは、隠された vtordisp コンストラクター/デストラクター ディスプレイスメント メンバーの追加を抑制します。 コンパイラ オプション ( /vd1 既定値) を使用すると、必要な場所で有効になります。 vtordisps をオフにするのは、すべてのクラス コンストラクターとデストラクターが仮想関数を仮想的に呼び出す場合のみです。

/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();  // 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 {};

上記のクラス宣言を考えると、次のようなコードは、in A または in Bを参照bしているかどうかbが不明であるため、あいまいです。

C *pc = new C;

pc->b();

前の例を考えます。 名前aはクラスとクラスABの両方のメンバーであるため、コンパイラは、呼び出される関数を指定するかを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 型のオブジェクトの宣言。

  • address-of 演算子 (> をそのオブジェクトに適用した場合の効果)。 address-of 演算子は、常にオブジェクトのベース アドレスを提供します。

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

Diagram showing how the conversion of pointers to base classes can be ambiguous.

最初の図は継承階層を示しています。A は基底クラスです。 B と C は A から継承します。D は B と C を継承します。次に、オブジェクト D のメモリ レイアウトが表示されます。D には、B (サブオブジェクト A を含む) と C (サブオブジェクト A を含む) の 3 つのサブオブジェクトがあります。 コード > はサブオブジェクト B の A を指します。コード ( * A ) & d は、サブオブジェクト B とサブオブジェクト C の両方を指します。

基底クラスへのポインターのあいまいな変換

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 から継承します。D は実質的に B と C から継承します。次に、D のレイアウトが表示されます。 D には、サブオブジェクト A を共有するサブオブジェクト B と C が含まれています。その後、レイアウトは、非仮想継承を使用して同じ階層が派生したかのように表示されます。 その場合、D にはサブオブジェクト B と C が含まれます。B と C の両方に、サブオブジェクト A の独自のコピーが含まれています。

仮想派生と非仮想派生

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

関連項目

継承