C ++で多重継承を避ける必要があるのはなぜですか?


167

多重継承を使用することは良いコンセプトですか、代わりに他のことを行うことができますか?

回答:


259

多重継承(MIと略記)はにおいがします。つまり、通常、それは悪い理由で行われ、メンテナーの顔に吹き返します。

概要

  1. 継承ではなく機能の構成を検討する
  2. 恐怖のダイヤモンドに注意する
  3. オブジェクトではなく複数のインターフェースの継承を検討する
  4. 時々、多重継承は正しいことです。ある場合は、それを使用します。
  5. コードレビューで複数継承されたアーキテクチャを守る準備をする

1.多分作曲?

これは継承にも当てはまるので、多重継承にも当てはまります。

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

構成の代わりに多重継承を使用してこれらの問題を解決すると、何か問題が発生します。

2.恐怖のダイヤモンド

通常、あなたはクラス持ちA、その後、BCの両方から継承しますA。そして、(理由は聞かないでください)次にDBとの両方を継承する必要があると誰かが決定しCます。

私はこの種類の問題に8と8年の間に2回遭遇しました。

  1. どのくらいそれは最初からだった間違いの(どちらの場合も、D両方から継承されていないはずBC、これは悪いアーキテクチャだったので、)(実際には、C...すべてに存在していてはいけません)
  2. C ++では、親クラスAがその孫クラスに2回存在していたため、メンテナがいくら払ったかということです。Dしたがって、1つの親フィールドA::fieldを更新すると、それを2回更新する(B::fieldおよびを介してC::field)か、何かが黙って間違ってクラッシュしてしまうことになります。 (に新しいポインタB::field、および削除C::field...)

C ++でキーワードvirtualを使用して継承を修飾すると、これが必要なものでない場合は、上記の二重レイアウトを回避できますが、とにかく、私の経験では、おそらく何か間違ったことをしています...

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

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

C ++でのDiamond of Dreadの本当の問題(設計が適切であると想定-コードをレビューしもらいます)は、選択する必要があることです

  • クラスAがレイアウトに2回存在することが望ましいですか、それはどういう意味ですか?はいの場合は、必ず2回継承します。
  • 一度しか存在しない場合は、仮想的に継承します。

この選択は問題に固有であり、C ++では、他の言語とは異なり、言語レベルで設計を強制することなく実際に行うことができます。

しかし、すべての権限と同様に、その権限には責任があります。設計をレビューしてもらいます。

3.インターフェース

ゼロまたは1つの具象クラス、および0以上のインターフェースの多重継承は通常、問題ありません。これは、上記のダイヤモンドの恐怖に遭遇しないためです。実際、これがJavaで行われる方法です。

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

C ++では、インターフェースは次のものを含む抽象クラスです。

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

ゼロから1つの実オブジェクト、およびゼロ以上のインターフェースの多重継承は、「臭い」とは見なされません(少なくとも、それほどではありません)。

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

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

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

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

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

時々、はい。

通常、あなたのCクラスから継承しているABし、AそしてB2つの無関係なオブジェクト(すなわちないなどと同じ階層に、共通して何も、異なる概念、中)です。

たとえば、NodesX、Y、Z座標のシステムを使用して、多くの幾何学的計算(おそらくポイント、幾何学的オブジェクトの一部)を実行でき、各ノードは他のエージェントと通信できる自動エージェントです。

おそらく、あなたはすでに2つのライブラリ、独自の名前空間を持つ各へのアクセス持っている(名前空間を使用する別の理由を...しかし、あなたは、あなたの名前空間をしていないでしょう?)、1ビーイングgeoや他のビーイングをai

したがってown::Nodeai::Agentとの両方から独自に派生させることができますgeo::Point

これは、代わりにコンポジションを使用するべきではないかどうかを自問する必要がある瞬間です。own::Node本当にa ai::Agentとaの両方である場合geo::Point、合成は行われません。

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

(あなたはそれに気づくでしょう、ai::Agentそしてgeo::Point完全に、完全に、完全に無関係です...これは多重継承の危険を劇的に減らします)

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

他の場合があります:

  • 実装の詳細として(うまくいけばプライベート)継承を使用する
  • ポリシーのような一部のC ++イディオムは多重継承を使用できます(各部分がを介して他の部分と通信する必要がある場合this
  • std :: exceptionからの仮想継承(仮想継承は例外に必要ですか?

コンポジションを使用できる場合もあれば、MIが優れている場合もあります。ポイントは次のとおりです。あなたには選択肢があります。責任を持って実行してください(そして、コードを確認してください)。

5.では、多重継承を行うべきでしょうか?

ほとんどの場合、私の経験では、違います。MIは、それが山に怠惰で使用することができますので、それは(作るような結果を実現することなく一緒に備えて、仕事に思える場合でも、適切なツールではありませんCar両方EngineWheel)。

しかし、時々、はい。そして、その時、MIより良いものは何もありません。

しかし、MIは臭いので、コードレビューでアーキテクチャを守る準備をしてください(そして、それを守ることができない場合は、行うべきではないので、それを守ることは良いことです)。


4
これは必ずしも必要ではなく、あまり役に立たないので、たとえそれが完全に適合したとしても、委任による節約は、それを使用することに慣れていないプログラマーの混乱を補うものではありませんが、それ以外の点では良い要約です。
Bill K

2
ポイント5に加えて、MIを使用するコードの横には、理由を説明するコメントが必要です。そのため、コードレビューで口頭で説明する必要はありません。これがなければ、あなたのレビュー担当者ではない誰かがあなたのコードを見てあなたの良い判断に疑問を投げかけるかもしれず、あなたはそれを守る機会がないかもしれません。
Tim Abell、2011年

上記のダイアモンドオブドレッドに遭遇しないからです。
curiousguy

1
オブザーバーパターンにはMIを使用します。
Calmarius

13
@BillK:もちろん、それは必要ではありません。MIを使用しない完全なターニング言語を使用している場合、MIを使用する言語でできることは何でもできます。はい、必要ありません。ずっと。とはいえ、これは非常に便利なツールであり、いわゆるDreaded Diamondとなる可能性があります。「dreaded」という言葉がなぜそこにあるのか、私にはよくわかりません。それは実際にはかなり簡単に推論でき、通常は問題ではありません。
トーマス・エディング

145

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

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


6
C ++にはC#とJavaで見逃していた機能がいくつかありますが、これらの機能を使用すると、設計がより困難/複雑になります。たとえば、クラスの不変性の保証const- クラスが実際に可変および不変のバリアントを必要とする場合、不自然回避策(通常はインターフェイスと構成を使用)を記述しなければなりませんでした。ただし、一度も多重継承を見逃したことはありません。また、この機能がないために回避策を書かなければならないと感じたこともありません。それが違いです。私が今までに見たどの場合でも、MIを使用しない方が設計上の選択であり、回避策ではありません。
BlueRaja-Danny Pflughoeft、2011

24
+1:完璧な答え:使用したくない場合は、使用しないでください。
トーマス・エディング

38

これを回避する理由はなく、状況によっては非常に役立ちます。ただし、潜在的な問題に注意する必要があります。

最大のものは死のダイヤモンドです:

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;

常に設計を見直し、継承を使用してデータの再利用を節約していないことを確認してください。同じことを構成で表すことができる場合(通常は可能)、これははるかに優れたアプローチです。


21
継承を禁止し、代わりにコンポジションのみを使用する場合は、常に2つGrandParentありChildます。人々は言語のルールを理解していないかもしれないと思っているだけなので、MIへの恐れがあります。しかし、これらの単純なルールを取得できない人は、重要なプログラムを作成することもできません。
curiousguy

1
厳しく聞こえますが、C ++の規則はすべての面で非常に複雑です。確かに、個々のルールが単純であるとしても、それらの多くが存在するため、少なくとも人間にとっては、全体が単純ではありません。
ゼブラフィッシュ2016

11

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

多重継承は批判を受けているため、多くの言語では実装されていません。批判は次のとおりです。

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

C ++ / Javaスタイルのコンストラクターを使用する言語で複数の継承を行うと、コンストラクターの継承問題とコンストラクターチェーンが悪化し、これらの言語でメンテナンスと拡張性の問題が発生します。大きく変化する構築メソッドと継承関係にあるオブジェクトは、コンストラクターチェーンパラダイムの下で実装するのが困難です。

これを解決してCOMおよびJavaインターフェースのようなインターフェース(純粋な抽象クラス)を使用するための最新の方法。

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

はい、できます。GoFから盗みます。

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

8

パブリック継承はIS-A関係であり、クラスはいくつかの異なるクラスのタイプになる場合があり、これを反映することが重要な場合があります。

「ミックス」も時々役立ちます。これらは一般に小さなクラスであり、通常は何からも継承せず、便利な機能を提供します。

継承階層がかなり浅く(ほとんどの場合そうであるはずです)、適切に管理されている限り、恐ろしいひし形の継承を取得することはほとんどありません。ダイヤモンドは、多重継承を使用するすべての言語に問題があるわけではありませんが、C ++によるダイヤモンドの扱いは、しばしば厄介で、時として不可解です。

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


6

多重継承を「避ける」べきではありませんが、「ダイヤモンドの問題」(http://en.wikipedia.org/wiki/Diamond_problem)などの発生する可能性のある問題に注意し、与えられた力を注意深く扱う必要があります、すべての権限で必要なように。


1
+1:ポインタを避けてはいけないのと同じです。あなたは彼らの影響を知る必要があるだけです。人々は、一部のヒッピーが自分の衛生状態が悪いと言っただけでインターネットから学んだフレーズ(「MIは悪だ」、「最適化しない」、「後藤は悪だ」など)をがたがたにするのをやめる必要があります。最悪の部分は、彼らがそのようなものを使用しようとさえせず、聖書から話されたかのようにそれを取りさえしたことです。
Thomas Eding

1
同意する。それを「回避」しないでください。しかし、問題を処理するための最良の方法を知っています。多分私はむしろ構成特性を使用したいのですが、C ++にはそれがありません。多分私はむしろ「ハンドル」自動委任を使いたいのですが、C ++にはそれがありません。したがって、MIの一般的で鈍いツールをいくつかの異なる目的で、おそらく同時に使用します。
JDługosz

3

少し抽象的になるリスクがありますが、カテゴリー理論の枠内で継承について考えるのは明るいと思います。

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

A --> B

はからclass B派生することを意味しますclass A。与えられたことに注意してください

A --> B, B --> C

CはAから派生するBから派生するため、CもAから派生すると言われます。

A --> C

さらに、AささいにAから派生するすべてのクラスについてA、継承モデルはカテゴリの定義を満たします。より伝統的な言語ではClass、オブジェクトにはすべてのクラスと継承関係の射が含まれるカテゴリがあります。

これは少し設定ですが、それでは、Diamond of Doomを見てみましょう。

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

これは怪しげな見た目ですが、実際はそうです。だから、D全てからの継承ABC。さらに、OPの質問への対応に近づくとD、のスーパークラスも継承されますA。図を描くことができます

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

ここで、Diamond of Deathに関連する問題は、プロパティとメソッドの名前を共有しているときにCBあいまいになる場合です。ただし、共有の動作を移動するとA、あいまいさがなくなります。

カテゴリ用語で言えば、私たちが望むABCあれば、そのようなことをするBCから継承Qその後は、Aのサブクラスとしてのように書き換えることができますQ。これはAプッシュアウトと呼ばれるものになります。

プルバックDと呼ばれる対称構造もあります。これは本質的に、との両方を継承して構築できる最も一般的な有用なクラスです。あなたが他のクラスにしている場合つまり、乗算から継承すると、そのクラスでのサブクラスとしてのように書き換えることができますが。BCRBCDRD

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

Paercebal答えは、彼の忠告は、我々はすべての可能なクラスの完全なカテゴリクラスで働くことを考えると上記のモデルによって暗示されているとして、これを触発しました。

彼の議論を一般化して、複雑な多重継承関係がどれほど強力で問題のないものになるかを示しました。

TL; DRプログラムの継承関係は、カテゴリを形成するものと考えてください。次に、多重継承されたクラスを対称的にプッシュアウトし、プルバックである共通の親クラスを作成することにより、Diamond of Doomの問題を回避できます。


3

エッフェルを使用しています。私たちは素晴らしいMIを持っています。心配ない。問題ない。簡単に管理できます。MIを使用しない場合があります。ただし、次の理由により、人々が気付く以上に役立ちます:A)うまく管理できない危険な言語-OR- B)何年にもわたってMIを回避してきた方法に満足している-または-C)その他の理由(リストするには数が多すぎるので、確信があります-上記の回答を参照してください)。

私たちにとって、Eiffelを使用すると、MIは他の何よりも自然であり、ツールボックス内の別の優れたツールでもあります。率直に言って、他の誰もがエッフェルを使用していないことを私たちはまったく心配していません。心配ない。私たちは私たちが持っているものに満足しており、あなたに見てもらうことを勧めます。

あなたが見ている間:Void-safetyとNullポインターの逆参照の根絶に特別な注意を払ってください。私たちはみんなMIの周りを踊っていますが、あなたのポインタは失われています!:-)


2

すべてのプログラミング言語には、長所と短所を持つオブジェクト指向プログラミングの扱いがわずかに異なります。C ++のバージョンは、パフォーマンスに真っ向から重点を置いており、それに伴い、無効なコードを作成するのが邪魔になりやすいという欠点があります。これは多重継承にも当てはまります。結果として、プログラマーをこの機能から遠ざける傾向があります。

他の人々は、多重継承が何のために良くないかという問題に対処しました。しかし、多かれ少なかれそれを避ける理由が安全ではないということを暗示するかなりの数のコメントを見てきました。はい、そうです。

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

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 { ..... };

私の提案は、クラスをミックスインクラスとして使用する場合は、命名規則を採用して、コードを読んでいる人が何が起こっているのかを簡単に確認し、基本的なガイドラインのルールに従ってプレイしていることを確認できるようにすることです。 。また、仮想ベースクラスの動作方法が原因で、ミックスインにもデフォルトのコンストラクターがある場合は、はるかにうまく機能することがわかります。また、すべてのデストラクタを仮想化することも忘れないでください。

ここでの「ミックスイン」という言葉の使い方は、パラメータ化されたテンプレートクラスと同じではないことに注意してください(適切な説明については、このリンクを参照してください)。用語の公平な使い方だと思います。

これが、多重継承を安全に使用する唯一の方法であるという印象を与えたくありません。これは、かなり簡単に確認できる1つの方法です。


2

慎重に使用する必要があります。DiamondProblemのように、状況が複雑になる場合があります。

代替テキスト
(ソース:learncpp.com


12
これは悪い例です。コピー機はスキャナーとプリンターで構成されるべきであり、それらから継承されるべきではないからです。
Svante

2
とにかく、はであるPrinterべきではありませんPoweredDevice。A Printerは印刷用であり、電源管理用ではありません。特定のプリンターの実装は、いくつかの電源管理を行う必要があるかもしれませんが、これらの電源管理コマンドは、プリンターのユーザーに直接公開されるべきではありません。この階層を実際に使用することは想像できません。
curiousguy


1

ひし形パターンを超えると、多重継承によりオブジェクトモデルが理解しにくくなる傾向があり、その結果、メンテナンスコストが増加します。

構成は本質的に理解、理解、説明が簡単です。コードを書くのは退屈な作業ですが、優れたIDE(Visual Studioを使用してから数年になりますが、Java IDEにはすばやいコンポジションショートカット自動化ツールがあります)なら、そのハードルを乗り越えられるはずです。

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

全体として、継承、特に多重継承を避ける傾向の格言は健全です。すべての格言として、例外がありますが、コード化する例外を指す点滅する緑のネオンサインがあることを確認する必要があります(そして、このような継承ツリーを見るたびに、点滅する緑のネオンで描くように脳を訓練します記号)、そして時々すべてが理にかなっていることを確認するためにチェックすること。


what happens when the designer for 'B' adds a 'makeJuice' method which generates and electrical current?もちろん、コンパイルエラーが発生します(使用法があいまいな場合)。
トーマス・エディング

1

具象オブジェクトのMIの重要な問題は、正当に「Aであり、Bになる」必要があるオブジェクトがないことです。そのため、論理的な理由でそれが正しい解決策になることはほとんどありません。はるかに多くの場合、「CはAまたはBとして機能できる」に従うオブジェクトCがあり、これはインターフェイスの継承と構成を介して実現できます。しかし、間違いはありません。複数のインターフェースの継承は依然としてMIであり、そのサブセットにすぎません。

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

class B : public A, public A {};

BY DEFINITIONの形式が正しくありません。これは英語に翻訳された「B is A and A」です。したがって、人間の言語でさえ、深刻なあいまいさが存在します。「B has 2 As」または単に「B is a A」ですか?そのような病理学的コードを許可し、さらに悪いことにそれを使用例にすると、C ++は、後続の言語で機能を維持するための主張をすることに関して不利に働きました。


0

継承よりも構成を使用できます。

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


4
-1:The general feeling is that composition is better, and it's very well discussed.構成が優れているという意味ではありません。
トーマス・エディング

それを除いて、実際には、より良いです。
Ali Afshar

12
それが良くない場合を除いて。
Thomas Eding 2013年

0

関係するクラスごとに4/8バイトかかります。(クラスごとに1つのthisポインター)。

これは問題になることは決してないかもしれませんが、ある日、数十億回インスタンス化されるマイクロデータ構造がある場合は、そのようになります。


1
よろしいですか?通常、深刻な継承には、各クラスメンバーにポインターが必要になりますが、継承されたクラスに合わせてスケーリングする必要はありますか?さらに言えば、何十億もの小さなデータ構造があったのでしょうか。
David Thornley、

3
@DavidThornley「何十億もの小さなデータ構造を何回持ったことがありますか?」MIに対する議論をしているとき。
curiousguy
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.