c++ - なぜ私はC ++で多重継承を避けるべきですか?


複数の継承を使用するのは良い考えですか?代わりに他のことをすることはできますか?



Answers


多重継承(略してMI)は匂いがします 。これは、 通常 、悪い理由で行われ、メンテナーの顔に吹き飛ばされることを意味します。

概要

  1. 継承の代わりに機能の構成を検討する
  2. ダイヤモンド・オブ・ドレッドを警戒する
  3. オブジェクトではなく複数のインタフェースの継承を考慮する
  4. 時には、多重継承が正しいことです。 そうであれば、それを使用してください。
  5. コードレビューで複数継承されたアーキテクチャを守る準備をしてください

1.おそらく構成?

これは継承の場合にも当てはまります。したがって、複数継承の場合はさらに真実です。

あなたのオブジェクトは本当に別のものから継承する必要がありますか? CarEngineWheelから継承する必要はありません。 CarにはEngineと四Wheelます。

複数の継承を使用して合成の代わりにこれらの問題を解決すると、何か間違ったことが起こります。

2.恐怖のダイヤモンド

通常、クラスAを持っていれば、 BC両方がAから継承します。 そして、なぜ誰かがDBC両方を継承しなければならないと決める。

私は8年のうちにこの種の問題に2回遭遇しました。

  1. これは悪いアーキテクチャ(実際には、 Cは全く存在していてはならない...)であったので、最初から間違いがどれくらいあったか(どちらの場合も、 DBC両方から継承するべきではない)
  2. C ++では、親クラスAが孫クラスD 2回存在し、親フィールドA::fieldを更新することは、 B::fieldC::fieldによって2回更新することを意味していたため、 C::field )、または何かが黙って間違ってクラッシュする、後で( B::field内の新しいポインタ、およびC::fieldを削除する...)

継承を修飾するためにC ++のキーワードvirtualを使用すると、上記のダブルレイアウトが回避されますが、これはあなたが望むものではない場合でも、とにかく、私の経験上、おそらく間違っているでしょう...

オブジェクト階層では、グラフではなく、階層をツリー(ノードには親が1つあります)として保持するようにしてください。

ダイヤモンドの詳細(編集:2017-05-03)

C ++でのDread of Diamondの本当の問題( 設計が健全で、あなたのコードが見直されていると仮定します )は、 あなたが選択する必要があるということです

  • クラスAがレイアウトに2度存在することが望ましく、それはどういう意味ですか? はいの場合は、それから2回継承してください。
  • それが一度だけ存在するならば、事実上それを継承する。

この選択は問題に固有のものであり、C ++では他の言語とは異なり、実際にはドグマを使わずに言語レベルでデザインを強制することができます。

しかし、すべての力のように、その力で責任が生じる:あなたのデザインを見直してください。

3.インターフェイス

ゼロまたは1つの具体的なクラスとゼロ以上のインターフェースの多重継承は、通常、上で説明したDread of Dreadに遭遇しないため、大丈夫です。 実際には、これはJavaで処理される方法です。

通常、CがABから継承しAときの意味は、ユーザーがCであるかのようにCを使用できるということです。

C ++では、インタフェースは以下を持つ抽象クラスです。

  1. そのすべてのメソッドは純粋な仮想(接尾辞= 0)を宣言し ました(2017-05-03を削除しました)
  2. メンバー変数なし

0から1つの実オブジェクトと0以上のインターフェースの多重継承は、「少なくとも臭い」とはみなされません。

C ++抽象インタフェースの詳細(edit 2017-05-03)

まず、NVIパターンを使用してインタフェースを生成することができます。 実際の基準は状態を持たないことです (つまり、 this以外のメンバ変数はありません)。 あなたの抽象的なインタフェースのポイントは、契約書を発行することです(「あなたはこのように、このように私を呼び出すことができます」)。 抽象仮想メソッドのみを持つことの制限は、義務ではなく設計上の選択でなければなりません。

第2に、C ++では、抽象的なインタフェース(追加のコスト/間接的サポートがある場合でも)から仮想的に継承するのが理にかなっています。 そうしないと、インターフェイスの継承が階層内に複数回現れると、あいまいさがあります。

第三に、オブジェクトの向きは素晴らしいですが、C ++ではThe Only Truth Out There TMではありません。 適切なツールを使用して、常にC ++でさまざまな種類のソリューションを提供する他のパラダイムがあることを忘れないでください。

4.本当に多重継承が必要ですか?

時には、はい。

通常、 CクラスはAB継承し、 ABは無関係な2つのオブジェクトです(つまり、同じ階層にない、共通ではない、異なる概念など)。

たとえば、多くの幾何学的計算(多分点、幾何学的オブジェクトの一部)を行うことができるX、Y、Z座標のNodesシステムを持つことができ、各ノードは自動エージェントであり、他のエージェントと通信することができます。

たぶんあなたはすでに2つのライブラリにアクセスしていますが、それぞれ独自の名前空間(名前空間を使用するもう一つの理由...しかし、あなたは名前空間を使用していますか?)は、 geoと、

したがって、独自own::Nodeai::Agentgeo::Point両方から派生します。

これは、代わりに合成を使用すべきではないかと自問する必要がある瞬間です。 own::Nodeが実際にai::Agentgeo::Point両方である場合、合成は行われません。

次に、あなたown::Nodeが3D空間内での位置に応じて他のエージェントと通信するように、複数の継承が必要になります。

ai::Agentgeo::Pointは完全に、完全に、完全には無関係であることに注意してください...これは多重継承の危険性を大幅に減らします)

その他のケース(2017-05-03を編集)

それ以外の場合があります:

  • 実装の詳細として(うまくいけば私的)継承を使用する
  • ポリシーのようないくつかのC ++のイディオムは、複数の継承を使用することができます(各パーツがthis介して他のパーツと通信する必要がある場合)
  • std :: exceptionからの仮想継承(例外には仮想継承が必要ですか?

時々あなたは構成を使用することができ、時にはMIが良いです。 ポイントは:選択肢があります。 それを責任感を持って実行します(コードをレビューしてください)。

5.それで、私は多重継承を行うべきですか?

ほとんどの時間、私の経験では、いいえ。 MIは、結果を実現することなく( EngineWheel両方のCar作るように)怠け者から積み重なる機能を一緒に使うことができるので、うまくいくように見えても正しいツールではありません。

しかし、時には、はい。 そしてその時、MIよりもうまくいくものはありません。

しかし、MIは臭いので、コードレビューであなたのアーキテクチャーを守る準備をしてください(防御することができなければ防御できないので、防御は良いことです)。




Bjarne Stroustrupとインタビューから:

複数の継承を使用して行うことができることは、単一の継承を使用して行うこともできるため、複数の継承を必要としないということは、かなり正確に言えます。 あなたは私が言及した委任のトリックを使用するだけです。 さらに、継承を一切必要としません。単一の継承で行うことは、クラスを介して継承することもできます。 実際には、ポインタとデータ構造ですべてを行うことができるので、クラスも必要ありません。 しかし、なぜあなたはそれをしたいですか? 語学施設を利用するのはいつ便利ですか? あなたはいつ回避策を好むでしょうか? 私は複数の継承が有用な場合を見てきましたが、複雑な多重継承が有用な場合も見てきました。 一般的に、私は、その言語で提供されている機能を使用して回避策をとることを好みます




それを避ける理由はなく、状況に非常に役立ちます。 あなたは潜在的な問題を意識する必要があります。

最も大きなものは死のダイヤモンドである:

class GrandParent;
class Parent1 : public GrandParent;
class Parent2 : public GrandParent;
class Child : public Parent1, public Parent2;

これで、Child内にGrandParentの2つの「コピー」が作成されました。

C ++はこのことを考えており、仮想継承を使って問題を回避することができます。

class GrandParent;
class Parent1 : public virtual GrandParent;
class Parent2 : public virtual GrandParent;
class Child : public Parent1, public Parent2;

常にデザインをレビューし、データの再利用を節約するために継承を使用していないことを確認してください。 同じことを構図で表現できるなら(そして通常は可能です)、これははるかに良いアプローチです。




w: 多重継承を参照してください。

複数の継承は批判を受けており、そのため、多くの言語で実装されていません。 批判には以下が含まれる:

  • 増加した複雑さ
  • 意味論的あいまいさはしばしばダイヤモンドの問題として要約される。
  • 1つのクラスから複数回明示的に継承できない
  • クラスのセマンティクスを変更する継承の順序。

C ++ / Javaスタイルのコンストラクタを使用する言語での多重継承は、コンストラクタやコンストラクタチェーンの継承問題を悪化させ、これらの言語でメンテナンスや拡張性の問題を引き起こします。 大きく異なった構築方法による継承関係のオブジェクトは、コンストラクタの連鎖パラダイムの下で実装するのが難しいです。

COMとJavaインターフェイスのようなインターフェイス(純粋な抽象クラス)を使用するためにこれを解決する現代的な方法。

私はこれの代わりに他のことをすることができますか?

はい、できます。 私はGoFから盗むつもりです。

  • インプリメンテーションではなくインターフェイスへのプログラム
  • 継承よりも構成を優先する



公開継承はIS-Aの関係であり、時にはクラスがいくつかの異なるクラスの型になることがあり、時にはこれを反映することが重要です。

「ミックスイン」も有用な場合があります。 それらは一般的に小さなクラスであり、通常は何かを継承するものではなく、有用な機能を提供します。

継承階層がかなり浅く(ほとんど常にそうであるように)、よく管理されている限り、恐ろしいダイヤモンドの継承を得ることはまずありません。 ダイヤモンドは多重継承を使用するすべての言語で問題ではありませんが、C ++の処理はしばしば扱いにくく、困惑しがちです。

私は複数の継承が非常に便利な場合に遭遇しましたが、実際には非常にまれです。 これは、私が本当に多重継承を必要としないときに、他の設計方法を使用する方が好きなためです。 私は混乱する言語構造を避けることを好むし、何が起こっているのか把握するためにマニュアルを本当にうまく読みなければならない継承のケースを構築するのは簡単です。




複数の継承を「避ける」べきではありませんが、「ダイヤモンド問題」( http://en.wikipedia.org/wiki/Diamond_problem )などの問題を認識し、あなたに与えられた力を注意深く扱うべきですあなたはすべての力を持っているべきです。







すべてのプログラミング言語は、賛否両論のオブジェクト指向プログラミングのやり方が少し異なります。 C ++のバージョンは、パフォーマンスに重点を置いており、無効なコードを書き込むのは簡単ではないという付随する欠点があります。これは多重継承にも当てはまります。 結果として、この機能からプログラマーを離れる傾向があります。

他の人は、多重継承が何に良いものではないのかという問題に取り組んできました。 しかし、それを避ける理由が安全ではないということが多かれ少なかれ示唆されているという、かなりのコメントがあります。 はい、いいえ。

C ++でよくあることですが、基本的なガイドラインに従えば、常に「あなたの肩を見渡す」ことなく、安全に使うことができます。 重要なアイデアは、 "ミックスイン"と呼ばれる特別な種類のクラス定義を区別することです。 クラスはすべてのメンバ関数が仮想(または純粋仮想)である場合に混在します。 その後、あなたは1つのメインクラスと好きな数の "ミックスイン"から継承することができますが、ミックスインをキーワード "virtual"で継承する必要があります。 例えば

class CounterMixin {
    int count;
public:
    CounterMixin() : count( 0 ) {}
    virtual ~CounterMixin() {}
    virtual void increment() { count += 1; }
    virtual int getCount() { return count; }
};

class Foo : public Bar, virtual public CounterMixin { ..... };

私の提案は、クラスをミックスインクラスとして使うつもりなら、コードを読んでいる誰もが何が起こっているのかを見たり、基本的なガイドラインの規則で遊んでいることを確認したりするための命名規則を採用することです。 仮想基底クラスの仕組みのために、あなたのミックスインにもデフォルトのコンストラクタがあれば、もっとうまくいくことがわかります。 また、すべてのデストラクタを仮想化することも忘れないでください。

私がここで "ミックスイン"という言葉を使用しているのは、パラメータ化されたテンプレートクラスと同じではありません( この説明を参照してください)。しかし、用語集を公正に使用していると思います。

今では、これが複数の継承を安全に使用する唯一の方法であるという印象を与えたくありません。 これは、チェックするのが簡単な単なる方法です。




ちょっと抽象的になる危険があるので、カテゴリ理論の枠組みの中で継承について考えることが明らかになっています。

継承関係を表すそれらの間のすべてのクラスと矢印を考えると、次のようなものになります

A --> B

class Bclass Aから派生していることを意味します。 与えられた

A --> B, B --> C

私たちはCがAから派生したBから派生していると言うので、CもAから派生すると言います

A --> C

さらに、私たちは、すべてのクラスAについて、 A派生AものをAとすると、継承モデルはカテゴリの定義を満たすと言う。 より伝統的な言語では、オブジェクトを持つClassがあります。すべてのクラスとモーフィズムは継承関係です。

それはセットアップのビットですが、それでは、私たちのダイヤモンド・オブ・ドゥームを見てみましょう:

C --> D
^     ^
|     |
A --> B

それは日陰のあるダイアグラムですが、それはします。 だから、 DABCすべてから継承します。 さらに、OPの質問に対処することに近づくと、 DAスーパークラスから継承されます。 私たちは図を描くことができます

C --> D --> R
^     ^
|     |
A --> B
^ 
|
Q

今、ダイアモンド・オブ・デス・ライフに関連する問題は、 CBがいくつかのプロパティ/メソッド名を共有しているときに曖昧なものになります。 しかし、共有行動をA移すと、あいまいさは消える。

カテゴリ的な言葉で言えば、 ABCBCQから継承するならば、 AQサブクラスとして書き直すことができます。 これは、 プッシュアウトと呼ばれるものを作ります。

D引き戻しと呼ばれる対称構造もあります。 これは基本的に、 BC両方から継承する最も一般的な有用なクラスです。 つまり、他のクラスRBCから継承していれば、 DDサブクラスとしてRを書き換えることができるクラスです。

ダイヤモンドの先端がプルバックとプッシュアウトであることを確認することで、そうでない場合に発生する可能性のある名前の衝突やメンテナンスの問題を一般的に処理する良い方法が得られます。

注意 Paercebalの答えは、すべての可能なクラスの完全なクラスで働いていることを前提に、上記のモデルによって暗示されているように、これを喚起しました。

私は多重継承関係がどれほど複雑で非効率的であるかを示す何かに彼の議論を一般化したかった。

TL; DRあなたのプログラムの継承関係をカテゴリを形成するものと考えてください。 次に、複数の継承クラスをプッシュアウトして対称的に作成し、共通の親クラスをプルダウンすることで、Doomのダイアモンド問題の問題を回避できます。







ダイアモンドパターン以外に、多重継承はオブジェクトモデルを理解しにくくする傾向があり、メンテナンスコストが増加します。

構成は、本質的に理解し、理解し、説明するのが簡単です。 コードを書くのは面倒ですが、良いIDEです(Visual Studioで作業してから数年経ちましたが、Java IDEには構成ショートカットの自動ツールがあります)。

また、メンテナンスの面では、非文字継承インスタンスでも「ダイヤモンド問題」が発生します。 たとえば、AとBがあり、クラスCがそれらを拡張し、Aにオレンジジュースを作る「makeJuice」メソッドがあり、それを拡張して、ライムのひねりでオレンジジュースを作る:デザイナーが ' B 'は、電流を生成し、生成する' makeJuice 'メソッドを追加します。 'A'と 'B'は現在 「両親」と互換性があるかもしれませんが、そうであれば必ずそうであるとは限りません!

全体的に、継承を避ける傾向の最大点、特に多重継承は健全です。 すべての格言は例外ではありますが、あなたがコードしている例外を指し示す点滅するネオンサインが点滅していることを確認する必要があります(あなたの脳を訓練してください。サイン)、あなたはそれが一度にすべて意味をなすことを確認することを確認してください。




具体的なオブジェクトのMIに関する重要な問題は、合法的に「AでありBになる」べきオブジェクトを持つことはめったにないので、論理的根拠にはまれな解決策であることはめったにありません。 より多くの場合、 "CはAまたはBの役割を果たす"ことに従うオブジェクトCを持っています。これは、インタフェースの継承と合成によって実現できます。 しかし、間違いなく、複数のインターフェイスの継承は依然としてMIのサブセットです。

特にC ++の場合、機能の主な弱点は、多重継承の実際の存在ではありませんが、ほとんどの場合、それが可能な構造はほとんど常に不正です。 たとえば、次のような同じオブジェクトの複数のコピーを継承します。

class B : public A, public A {};

定義によって誤っています。 これは英語で "BはAとAです"と翻訳されています。 したがって、人間の言語でさえ、重大なあいまいさがあります。 「Bは2 Asを持っている」、あるいは「BはAです」という意味ですか? そのような病理学的コードを許して、それを使用例にして悪化させたのは、後継言語でその機能を保持するケースを作ることになったとき、C ++は無益でした。




継承に優先して合成を使用することができます。

一般的な感想は、構成がより良いことであり、非常によく議論されています。




それは関係するクラス当たり4/8バイトを要する。 (クラスごとに1つのポインタ)。

これは決して心配しないかもしれませんが、ある日、あなたは数十億分の1秒のインスタンス化されたマイクロデータ構造を持っていればそれが実現します。




エッフェル塔を使用しています。 私たちは優れたMIを持っています。 心配ない。 問題ありません。 簡単に管理できます。 MIを使用しない場合があります。 しかし、それは、A)それをうまく管理していない危険な言語で、B)彼らが何年も何年もMIの周りで働いてきたことに満足しているため、またはC)その他の理由(私はかなり確信しています - 上記の回答を参照してください)。

私たちのために、エッフェル塔を使用して、MIは他のものと同じくらい自然であり、ツールボックスのもうひとつの素晴らしいツールです。 率直に言えば、誰もエッフェル塔を使用していないことはまったく気にしていません。 心配ない。 私たちは私たちが持っているものに満足しています。

あなたが探している間:空の安全性とNullポインタ逆参照の根絶に特に注意してください。 私たちはすべてMIの中で踊っていますが、あなたの指針は失われています! :-)