フォードを知らない、水に入ってはいけない。 パートN4

今回は、C ++での仮想継承と、それを非常に慎重に使用する必要がある理由について説明します。 前の記事:パートN1N2N3

この記事は、記事「 Rake 2:Virtual Inheritance 」に基づいています。 記事は良いですが、私の意見では、多少ぼやけており、初心者は危険の本質を完全に把握していないかもしれません。 仮想継承に関連する問題の説明の独自のバージョンを提供することにしました。



仮想基本クラスの初期化について



最初に、仮想継承がない場合にメモリにクラスを保存する方法について説明します。 コードを考慮してください:

class Base { ... }; class X : public Base { ... }; class Y : public Base { ... }; class XY : public X, public Y { ... };
      
      





ここではすべてが簡単です。 非仮想基本クラス「Base」のメンバーは、派生クラスの単純なメンバーデータとしてホストされます。 その結果、「XY」オブジェクト内には、2つの独立した「ベース」サブオブジェクトがあります。 概略的には、これは次のように表すことができます。

図1.非仮想多重継承。

図1.非仮想多重継承。



仮想基本クラスオブジェクトは、派生クラスのオブジェクトを1回だけ入力します。 以下のコードの「XY」オブジェクトのデバイスを図2に示します。

 class Base { ... }; class X : public virtual Base { ... }; class Y : public virtual Base { ... }; class XY : public X, public Y { ... };
      
      





図2.仮想多重継承。

図2.仮想多重継承。



共有された「ベース」サブオブジェクトのメモリは、「XY」オブジェクトの最後に割り当てられる可能性があります。 クラスの配置方法は、コンパイラによって異なります。 たとえば、共通の「Base」オブジェクトへのポインターは、「X」および「Y」クラスに格納できます。 しかし、私が理解しているように、この方法は使用されなくなりました。 多くの場合、共有サブオブジェクトへの参照は、仮想関数のテーブルに格納されるオフセットまたは情報の形式で実装されます。



「最も派生した」クラス「XY」のみが、仮想ベースクラス「Base」のサブオブジェクトのメモリがどこにあるべきかを正確に知っています。 したがって、仮想ベースクラスのすべてのサブオブジェクトを初期化することは、最も派生したクラスに委ねられます。



コンストラクター「XY」は、サブオブジェクト「Base」と「X」および「Y」のこのオブジェクトへのポインターを初期化します。 次に、クラス「X」、「Y」、「XY」の残りのメンバーが初期化されます。



'XY'コンストラクターで 'Base'サブオブジェクトが初期化された後、 'X'または 'Y'コンストラクターによって再び初期化されることはありません。 これがどのように行われるかは、コンパイラによって異なります。 例えば、コンパイラーは、コンストラクター「X」および「Y」に特別な追加引数を渡すことができます。これは、クラス「Base」を初期化しないことを示します。



そして今、最も興味深いのは、多くの誤解や間違いにつながっています。 これらのコンストラクターを検討してください。

 X::X(int A) : Base(A) {} Y::Y(int A) : Base(A) {} XY::XY() : X(3), Y(6) {}
      
      





基本クラスのコンストラクターは、引数としてどのような数を取りますか? 3番か6番? それらの1つではありません。



コンストラクター「XY」は、仮想サブオブジェクト「Base」を初期化しますが、暗黙的に行います。 デフォルトのBaseコンストラクターが呼び出されます。



'XY'コンストラクターが 'X'または 'Y'コンストラクターを呼び出す場合、 'Base'は再度初期化されません。 そのため、引数を指定した 'Base'の明示的な呼び出しは発生しません。



仮想ベースクラスでの冒険はこれで終わりではありません。 コンストラクターに加えて、代入演算子があります。 間違っていなければ、コンパイラによって生成された代入演算子は、仮想ベースクラスのサブオブジェクトに繰り返し代入できると規格は述べています。 または多分一度だけ。 そのため、「Base」オブジェクトのコピーが何回発生するかはわかりません。



代入演算子を実装する場合は、「Base」オブジェクトを一度コピーすることに注意する必要があります。 間違ったコードを考慮してください:

 XY &XY::operator =(const XY &src) { if (this != &src) { X::operator =(*this); Y::operator =(*this); .... } return *this; }
      
      





このコードは、「Base」オブジェクトを二重コピーします。 これを回避するには、「Base」クラスのメンバーがコピーしない「X」および「Y」クラスの関数を実装する必要があります。 「Base」クラスの内容は、ここに1回コピーされます。 修正されたコード:

 XY &XY::operator =(const XY &src) { if (this != &src) { Base::operator =(*this); X::PartialAssign(*this); Y::PartialAssign(*this); .... } return *this; }
      
      





このようなコードは機能しますが、これはすべて見苦しくてわかりにくいものです。 したがって、複数の仮想継承を避ける方が良いと言われています。



仮想基本クラスと型キャスト



仮想ベースクラスをメモリに配置する特性のため、次の型変換は実行できません。

 Base *b = Get(); XY *q = static_cast<XY *>(b); //   XY *w = (XY *)(b); //  
      
      





ただし、永続的なプログラマは「reinterpret_cast」演算子を使用して型をキャストできます。

 XY *e = reinterpret_cast<XY *>(b);
      
      





ただし、ほとんどの場合、これは使用できない結果になります。 「ベース」オブジェクトの開始アドレスは、「XY」オブジェクトの開始として解釈されます。 そして、これは私たちが必要とするものではありません。 説明図3を参照してください。



型変換を実行する唯一の方法は、dynamic_cast演算子を使用することです。 ただし、dynamic_castが定期的に使用されるコードは悪臭を放ちます。

図3.型キャスト。

図3.型キャスト。



仮想継承を放棄すべきですか?



私は多くの著者に、仮想継承をあらゆる方法で避けるべきであることに同意します。 また、単純な多重継承からも残すほうが適切です。



仮想継承は、オブジェクトの初期化およびコピー時に問題を引き起こします。 初期化とコピーは、「最も派生した」クラスで処理する必要があります。 そしてこれは、彼が基本クラスの構造についての詳細を知る必要があることを意味します。 クラス間の余分な結合は、プロジェクトの構造を複雑にし、リファクタリング時に異なるクラスで追加の編集を強制します。 これらはすべてエラーの原因となり、新しい開発者によるプロジェクトの理解を複雑にします。



型変換の複雑さもエラーの一因となります。 問題の一部は、dynamic_cast演算子を使用して解決されます。 ただし、これは非常に遅い演算子です。 そして、それがプログラムに大量に現れ始めたら、おそらく、これはプロジェクトの貧弱なアーキテクチャを示しています。 ほとんど常に、多重継承に頼らずにプロジェクト構造を実装できます。 実際、多くの言語ではそのようなフリルはまったくありません。 そして、これは大規模なプロジェクトの実装を妨げません。



仮想継承を放棄することを主張するのは愚かです。 時には便利で便利です。 ただし、最初に複雑なクラスを積み上げることは慎重に検討する価値があります。 いくつかの巨大なツリーで作業するよりも、階層が浅い小さなクラスのフォレストを成長させる方が常に適切です。 たとえば、多くの場合、多重継承の代わりに集約を使用できます。



多重継承の利点



まあ、複数の仮想継承と複数の継承の批判は理解できます。 安全で便利な場所はありますか?



はい、少なくとも1つのものに名前を付けることができます。インターフェイスの混在です。 この方法論がおなじみでない場合は、「足で自分を撃つのに十分な長さのロープ」という本に目を向けることを提案します[3]。



インターフェイスクラスにデータがありません。 通常、すべての機能は純粋に仮想です。 彼にはコンストラクターがないか、何もしていません。 つまり、このようなクラスを作成またはコピーしても問題はありません。



基本クラスがインターフェイスの場合、割り当ては空の操作です。 そのため、オブジェクトが何度もコピーされる場合でも、怖くありません。 コンパイルされたプログラムコードでは、このコピーは単に存在しません。



さらに読む



  1. ステファンC.デューハースト。 滑りやすい場所C ++。 プログラムを設計およびコンパイルする際の問題を回避する方法。 -M .: DMKプレス。 -264 p .:病気。 BBK 32.973.26-018.2、ISBN 978-5-94074-837-3。 (ヒント番号45および53を参照)。
  2. ウィキペディア 集約(プログラミング)
  3. アランI.ゴラブ。 「足で自分自身を撃つのに十分な長さのロープ」 (インターネットで簡単に検索できます。101以降のセクションを参照してください。)



All Articles