web-dev-qa-db-ja.com

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

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

158
Hai

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

概要

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

1.おそらく構成?

これは継承についても当てはまるため、多重継承についてはさらに当てはまります。

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

構成ではなく多重継承を使用してこれらの問題を解決する場合、何か間違ったことをしていることになります。

2.恐怖のダイヤモンド

通常、クラスAがあり、BCは両方ともAを継承します。そして(理由を聞かないで)DBCの両方を継承する必要があると判断した人。

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

  1. これは最初からどれだけの間違いでしたか(どちらの場合も、DBCの両方から継承すべきではありません)。これは悪いアーキテクチャであったためです(実際、Cはまったく存在しないはずです...)
  2. C++では、親クラスAがその孫クラスDに2回存在していたため、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つの具象クラスとゼロまたは1つ以上のインターフェイスの多重継承は、通常は大丈夫です。これは、上記のダイヤモンドの恐怖に遭遇しないからです。実際、これがJavaでの処理方法です。

通常、CがAおよびBから継承する場合、ユーザーはCAであるかのように、および/またはBであるかのように使用できます。

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

  1. すべてのメソッドが純粋仮想と宣言されています(サフィックス= 0)(2017-05-03を削除)
  2. メンバー変数なし

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

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

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

第二に、C++では、(追加のコスト/インダイレクションがあったとしても)抽象インターフェースから仮想的に継承することは理にかなっています。そうしないと、インターフェイスの継承が階層内で複数回表示される場合、あいまいさがあります。

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

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

時々、はい。

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

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

おそらく、それぞれ独自の名前空間(名前空間を使用するもう1つの理由...)を持つ2つのライブラリに既にアクセスできます。1つはgeoで、もう1つはaiです

したがって、独自の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は、たとえ機能しているように見えるとしても、結果を認識せずにフィーチャを積み重ねるのに怠慢な人が使用できるため、適切なツールではありません(CarEngineWheelの両方にするなど)。

しかし、時々、はい。そして、その時点では、MIほどうまくいくものはありません。

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

252
paercebal

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

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

140

それを避ける理由はなく、状況によっては非常に有用です。ただし、潜在的な問題に注意する必要があります。

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

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;

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

38
billybob

W: Multiple Inheritance を参照してください。

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

  • 複雑さの増加
  • セマンティックなあいまいさは、多くの場合 diamond problem と要約されます。
  • 単一のクラスから複数回明示的に継承できない
  • クラスセマンティクスを変更する継承の順序。

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

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

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

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

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

パブリック継承はIS-A関係であり、クラスは時には複数の異なるクラスのタイプになり、時にはこれを反映することが重要です。

「ミックスイン」も役立つ場合があります。通常、これらは小さなクラスであり、通常は何も継承せず、有用な機能を提供します。

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

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

8
David Thornley

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

6
jwpfox

エッフェルを使用します。優れたMIがあります。心配ない。問題ない。簡単に管理できます。 MIを使用しない場合があります。ただし、A)それをうまく管理できない危険な言語である-OR- B)長年にわたってMIを回避してきた方法に満足している-OR- C)その他の理由(数が多すぎてリストに入れることはできません。上記の回答を参照してください。

私たちにとって、Eiffelを使用すると、MIは他のツールと同じくらい自然で、ツールボックス内の別のすばらしいツールです。率直に言って、私たちは他の誰もエッフェルを使用していないことにまったく関心を持っています。心配ない。私たちは私たちが持っているものに満足しているので、ぜひご覧ください。

探している間:Void-safetyとNullポインターの逆参照の撲滅に特に注意してください。私たちは皆MIを中心に踊っていますが、あなたの指針は失われつつあります! :-)

2
Larry

慎重に使用する必要があります。 Diamond Problem など、状況が複雑になる場合があります。

alt text
(ソース: learncpp.com

2
CMS

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

他の人々は、多重継承が何の役に立たないかという問題に取り組んでいます。しかし、多かれ少なかれそれを避ける理由はそれが安全ではないからだということを暗示しているというコメントをかなり見ました。はい、そうです。

C++でよくあることですが、基本的なガイドラインに従えば、「肩越しに」常に監視しなくても安全に使用できます。重要な考え方は、「ミックスイン」と呼ばれる特別な種類のクラス定義を区別することです。クラスは、そのすべてのメンバー関数が仮想(または純粋仮想)の場合、ミックスインです。次に、単一のメインクラスと、必要な数の「ミックスイン」から継承できますが、キーワード「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 { ..... };

クラスをミックスインクラスとして使用する場合は、命名規則を採用して、コードを読んでいる人が何が起こっているのかを確認し、基本的なガイドラインのルールに従ってプレイしていることを確認できるようにすることをお勧めします。また、仮想ベースクラスが機能するという理由だけで、ミックスインに既定のコンストラクターが含まれていると、はるかにうまく機能することがわかります。そして、すべてのデストラクタも仮想化することを忘れないでください。

ここでの「ミックスイン」という単語の使用は、パラメーター化されたテンプレートクラスとは異なります(良い説明については this link を参照してください)が、用語の公正な使用であると思います。

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

2
sfkleach

少し抽象的になるリスクがあるが、カテゴリー理論の枠内で継承について考えることは光を当てる。

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

A --> B

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

A --> B, B --> C

cはAから派生するBから派生するため、CもAから派生すると言われているため、

A --> C

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

それは少しセットアップですが、それでは私たちのDoom of Doomを見てみましょう:

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

これは日陰に見える図ですが、実際にはそうです。したがって、Dは、AB、およびCのすべてから継承します。さらに、OPの質問への対処に近づくと、DAのスーパークラスからも継承します。図を描くことができます

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

さて、ここで死のダイヤモンドに関連する問題は、CBがいくつかのプロパティ/メソッド名を共有し、物事が曖昧になるときです。ただし、共有動作をAに移動すると、あいまいさがなくなります。

カテゴリ用語で言えば、AB、およびCは、BおよびCQから継承する場合、Aは、Qのサブクラスとして書き換えることができます。これにより、Aプッシュアウト と呼ばれます。

Dには pullback と呼ばれる対称構造もあります。これは、本質的に、BCの両方から継承する、構築できる最も一般的な便利なクラスです。つまり、RBを継承する他のクラスCが複数ある場合、DRがサブクラスとして書き換えられるクラスですof D

ダイヤモンドのヒントがプルバックおよびプッシュアウトであることを確認することで、名前の衝突やメンテナンスの問題を一般的に処理する素晴らしい方法が得られます。

Paercebalanswer は、上記のモデルが示唆するように、彼の忠告が暗示されているため、これに影響を与えましたすべての可能なクラスの完全なカテゴリクラスで作業します。

私は彼の議論を、多重継承関係がどれほど複雑で強力かつ問題にならないかを示すものに一般化したかった。

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

2
Ncat

継承の使用と乱用。

この記事は、継承を説明するのに非常に役立ち、危険です。

1
csexton

具体的なオブジェクトのMIの重要な問題は、「AでありBである」合法的にすべきオブジェクトがめったにないことであるため、論理的な理由でめったに正しいソリューションとはなりません。多くの場合、「CがAまたはBとして機能できる」に従うオブジェクトCがあります。これは、インターフェイスの継承と構成によって実現できます。ただし、間違えないでください。複数のインターフェイスの継承は依然としてMIであり、そのサブセットにすぎません。

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

class B : public A, public A {};

dEFINITIONによって不正な形式です。英語に翻訳すると、これは「BはAとAです」です。そのため、人間の言語であっても、深刻なあいまいさがあります。 「Bには2つのAsがあります」または「BはAです」という意味ですか?そのような病理学的なコードを許可し、さらに悪い例として使用例にすると、機能を後継言語で保持することを主張することになると、C++は好意的になりませんでした。

1
Zack Yezek

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

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

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

全体的に、継承、特に多重継承を避ける傾向があるという格言は健全です。すべての格言として、例外がありますが、コーディングする例外を指す点滅する緑色のネオンサインがあることを確認する必要があります(そして、そのような継承ツリーを見るたびに独自の点滅する緑色のネオンで描くように脳を訓練します)署名)、そして時々それがすべて意味をなすことを確認するためにチェックすること。

1
Tom Dibble

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

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

0
Ronny Brendel

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

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

0
Ali Afshar