C ++:クラスはその依存関係を所有または観察する必要がありますか?


16

class Foobarを使用する(依存する)クラスがあるとしWidgetます。Widget古き良き時代では、woludはのフィールドとして宣言されるFoobarか、多態的な動作が必要な場合はスマートポインターとして宣言され、コンストラクタで初期化されます。

class Foobar {
    Widget widget;
    public:
    Foobar() : widget(blah blah blah) {}
    // or
    std::unique_ptr<Widget> widget;
    public:
    Foobar() : widget(std::make_unique<Widget>(blah blah blah)) {}
    (…)
};

そして、私たちはすべて準備ができて完了です。残念ながら、今日は、Javaの子供たちはカップルとして、当然、彼らはそれを見たら、私たちを笑う、となりますFoobarWidget一緒に。解決策は一見シンプルです。依存関係の注入を適用して、Foobarクラスから依存関係を構築します。しかし、その後、C ++は依存関係の所有権について考えるように強制します。3つのソリューションが思い浮かびます。

ユニークなポインター

class Foobar {
    std::unique_ptr<Widget> widget;
    public:
    Foobar(std::unique_ptr<Widget> &&w) : widget(w) {}
    (…)
}

Foobarそれの唯一の所有権がWidgetそれに渡されると主張する これには次の利点があります。

  1. パフォーマンスへの影響はごくわずかです。
  2. FoobarコントロールのライフタイムがWidgetであるため、安全です。そのため、Widget突然消えることはありません。
  3. Widget漏れることはなく、不要になったときに適切に破壊されます。

ただし、これにはコストがかかります。

  1. Widgetインスタンスの使用方法に制限があります。たとえば、スタックに割り当てられたものWidgetsは使用Widgetできず、共有できません。

共有ポインター

class Foobar {
    std::shared_ptr<Widget> widget;
    public:
    Foobar(const std::shared_ptr<Widget> &w) : widget(w) {}
    (…)
}

これはおそらく、Javaおよびその他のガベージコレクション言語に最も近い同等のものです。利点:

  1. 依存関係を共有できるため、より普遍的です。
  2. unique_ptrソリューションの安全性(ポイント2および3)を維持します。

短所:

  1. 共有が含まれていない場合、リソースを無駄にします。
  2. ヒープ割り当てが引き続き必要であり、スタック割り当てオブジェクトは許可されません。

昔ながらの観察ポインター

class Foobar {
    Widget *widget;
    public:
    Foobar(Widget *w) : widget(w) {}
    (…)
}

生のポインタをクラス内に配置し、所有権の負担を他の人に移します。長所:

  1. できるだけ簡単に。
  2. ユニバーサル、すべてを受け入れますWidget

短所:

  1. もう安全ではありません。
  2. との両方の所有権を担当する別のエンティティを紹介FoobarWidgetます。

いくつかのクレイジーなテンプレートのメタプログラミング

私が考えることができる唯一の利点は、ソフトウェアが開発されている間に時間を見つけられなかった本をすべて読むことができるということです;)

3番目のソリューションは最も普遍的であり、何かを管理する必要Foobarsがあるため、管理Widgetsは単純な変更です。ただし、生のポインターを使用すると気になりますが、一方で、スマートポインターソリューションは、依存関係のコンシューマーがその依存関係の作成方法を制限するので、私にとって違和感を覚えます。

何か不足していますか?それとも、C ++での依存性注入は簡単ではありませんか?クラスはその依存関係を所有するか、単にそれらを観察する必要がありますか?


1
最初からC ++ 11でコンパイルできる場合、および/またはまったくコンパイルできない場合は、クラス内のリソースを処理する方法としてプレーンポインターを使用することは推奨されません。Foobarクラスがリソースの唯一の所有者であり、リソースを解放する必要がある場合、Foobarが範囲外になったときに、std::unique_ptrその方法を選択します。を使用std::move()して、リソースの所有権を上位スコープからクラスに転送できます。
アンディ

1
それFoobarが唯一の所有者であることをどうやって知るのですか?古いケースでは簡単です。しかし、DIの問題は、私が見るように、クラスをその依存関係の構築から切り離し、またそれらの依存関係の所有権からも切り離します(所有権は構築に関連付けられているため)。Javaなどのガベージコレクション環境では、それは問題ではありません。C ++では、これはそうです。
-el.pescado

1
@ el.pescado Javaにも同じ問題があります。メモリが唯一のリソースではありません。たとえば、ファイルと接続もあります。Javaでこれを解決する通常の方法は、含まれるすべてのオブジェクトのライフサイクルを管理するコンテナを用意することです。したがって、DIコンテナに含まれるオブジェクトでそれを心配する必要はありません。DIコンテナがどのライフサイクルフェーズにいつ切り替えるかを知る方法がある場合、これはc ++アプリケーションでも機能する可能性があります。
SpaceTrucker

3
もちろん、すべての複雑さを伴わずに昔ながらの方法でそれを行うことができ、動作し、メンテナンスがはるかに簡単なコードを持つことができます。(Javaの少年たちは笑うかもしれませんが、彼らは常にリファクタリングを行っているので、デカップリングは正確にはあまり
役に立た

3
さらに、他の人にあなたを笑わせることは、彼らのようになる理由ではありません。彼らの議論に耳を傾け(「言われたから」を除く)、それがあなたのプロジェクトに具体的な利益をもたらすなら、DIを実装してください。この場合、パターンは非常に一般的なルールであると考えてください。これはプロジェクトに固有の方法で適用する必要があります。それらは全体的な利益をもたらします(つまり、欠点よりも欠点が多い)。

回答:


2

これをコメントとして書くつもりでしたが、長すぎることがわかりました。

それFoobarが唯一の所有者であることをどうやって知るのですか?古いケースでは簡単です。しかし、DIの問題は、私が見るように、クラスをその依存関係の構築から切り離し、またそれらの依存関係の所有権からも切り離します(所有権は構築に関連付けられているため)。Javaなどのガベージコレクション環境では、それは問題ではありません。C ++では、これはそうです。

std::unique_ptr<Widget>またはを使用するかどうかstd::shared_ptr<Widget>は、決定するのはユーザー次第であり、機能によって決まります。

Utilities::Factoryようなブロックを作成するを持っていると仮定しましょうFoobar。DIの原則に従って、Widgetインスタンスが必要になります。これは、Foobarのコンストラクターを使用してインジェクトするために必要です。Utilities::FactoryたとえばcreateWidget(const std::vector<std::string>& params)、のメソッドの1つで、ウィジェットを作成してFoobarオブジェクトにインジェクトします。

これでUtilities::FactoryWidgetオブジェクトを作成するメソッドができました。つまり、メソッドはその削除を担当する必要がありますか?もちろん違います。インスタンスにするためだけにあります。


複数のウィンドウを持つアプリケーションを開発していると想像してみてください。各ウィンドウはFoobarクラスを使用して表されるため、実際にはFoobarコントローラーのように機能します。

コントローラはおそらくあなたWidgetののいくつかを利用するでしょう、そしてあなたはあなた自身に尋ねなければなりません:

アプリケーションのこの特定のウィンドウに移動する場合、これらのウィジェットが必要になります。これらのウィジェットは他のアプリケーションウィンドウ間で共有されていますか?もしそうなら、それらは共有されているため常に同じように見えるので、私はおそらくそれらを再び作成するべきではありません。

std::shared_ptr<Widget> 行く方法です。

また、アプリケーションウィンドウもあります。Widgetこのウィンドウには、この1つのウィンドウにのみ関連付けられているため、他の場所には表示されません。そのため、ウィンドウを閉じてWidgetも、アプリケーションのどこにも、少なくともそのインスタンスは必要ありません。

そこでstd::unique_ptr<Widget>王座を主張するようになります。


更新:

生涯の問題について、@DominicMcDonnellには本当に同意しません。を呼び出すstd::movestd::unique_ptr所有権が完全に転送されるためobject A、メソッド内でを作成object Bし、依存関係として別のメソッドに渡す場合でも、スコープ外になったときにobject B、リソースの責任を負い、リソースをobject A正しく削除しobject Bます。


所有権とスマートポインターの一般的な考え方は、私には明らかです。私には、DIのアイデアでうまくプレイするようには思えません。依存性注入とは、クラスをその依存関係から切り離すことですが、この場合、クラスは依然として依存関係の作成方法に影響を与えます。
el.pescado

たとえば、私が抱えている問題unique_ptrはユニットテストです(DIはテストに優しいと宣伝されています):テストしたいFoobarのでWidget、作成し、それを渡しFoobar、運動し、Foobar検査したいのですWidgetが、Foobarどういうわけか公開しない限り、によって主張されているのでFoobar
el.pescado

@ el.pescado 2つのことを組み合わせています。あなたはアクセシビリティと所有権を混ぜています。Foobarリソースを所有しているからといって、それを使用する必要はありません。のFoobarようなメソッドを実装することができます。このメソッドはWidget* getWidget() const { return this->_widget.get(); }、操作可能な生のポインターを返します。その後、Widgetクラスをテストするときに、このメソッドを単体テストの入力として使用できます。
アンディ

良い点、それは理にかなっています。
el.pescado

1
shared_ptrをunique_ptrに変換した場合、unique_nessをどのように保証しますか?
アコンカグア

2

参照の形で観測ポインターを使用します。それを使用すると、はるかに優れた構文を提供し、所有権を意味しないというセマンティックな利点があります。

このアプローチの最大の問題は寿命です。依存関係が依存クラスの前に構築され、依存クラスの後に破棄されることを確認する必要があります。簡単な問題ではありません。共有ポインターを使用すると(依存性ストレージとして、およびそれに依存するすべてのクラス、上記のオプション2)、この問題を取り除くことができますが、循環的な依存性の問題も発生します。問題が発生する前に検出します。だからこそ、ライフタイムと建設順序を自動的に手動で管理したくないのです。また、オブジェクトのリストを作成された順序で作成し、逆の順序で破棄するライトテンプレートアプローチを使用するシステムを見てきました。これは絶対確実ではありませんでしたが、物事がずっと簡単になりました。

更新

デビッド・パッカーの回答により、私は質問についてもう少し考えさせられました。元の答えは、私の経験では共有依存関係に当てはまります。これは、依存関係注入の利点の1つであり、1つの依存関係のインスタンスを使用して複数のインスタンスを持つことができます。ただし、クラスが特定の依存関係の独自のインスタンスを持つ必要std::unique_ptrがある場合は、正しい答えです。


1

まず第一に、これはJava ではなく C ++であり、ここでは多くのことが異なっています。Javaの人々は所有権に関する問題を抱えていません。それらを解決する自動ガベージコレクションがあります。

第二:この質問に対する一般的な答えはありません-要件が何であるかに依存します!

FooBarとWidgetの結合に関する問題は何ですか?FooBarはウィジェットの使用を望んでおり、すべてのFooBarインスタンスが常に独自の同じウィジェットを持っている場合は、そのままにしておきます...

C ++では、単にJavaには存在しない「奇妙な」こともできます。たとえば、可変個のテンプレートコンストラクターがあります(まあ、Javaには...表記があります。これはコンストラクターでも使用できますが、オブジェクト配列を隠すための構文糖衣だけです。これは、実際の可変長テンプレートとは何の関係もありません!)-カテゴリ「Some crazy template metaprogramming」:

namespace WidgetFactory
{
    Widget* create(int, int)
    {
        return 0;
    }
    Widget* create(int, bool, long)
    {
        return 0;
    }
}
class FooBar
{
public:
    template < typename ...Arguments >
    FooBar(Arguments... arguments)
        : mWidget(WidgetFactory::create(arguments...))
    {
    }
    ~FooBar()
    {
        delete mWidget;
    }
private:
    Widget* mWidget;
};

FooBar foobar1(10, 12);
FooBar foobar2(51, true, 54L);

いいね?

もちろん、そこにいるだけで、あなたがしたいか、ウィジェットを再利用する必要がある場合は、任意のFooBarのインスタンスが存在するはるか前の時点でウィジェットを作成する必要がある場合など...、、、または-あなたが欲しいの理由または両方のクラスを分離する必要性はなぜなら、現在の問題ではより適切だからです(たとえば、WidgetがGUI要素であり、FooBarが1つでなければならない場合)。

次に、2番目のポイントに戻ります。一般的な答えはありません。実際の問題に対して、より適切なソリューションは何かを決める必要があります。私はDominicMcDonnellのリファレンスアプローチが好きですが、FooBarが所有権を取得しない場合にのみ適用できます(実際には可能ですが、それは非常に汚いコードを意味します...)。それとは別に、David Packerの回答(コメントとして書かれたもの-とにかく良い回答)に参加します。


1
C ++では、class例のように単なるファクトリであっても、静的メソッド以外にesを使用しないでください。それはオブジェクト指向の原則に違反しています。代わりに名前空間を使用し、それらを使用して関数をグループ化します。
アンディ

@DavidPackerうーん、あなたは正しいです-私は静かにクラスにいくつかのプライベート内部があると仮定しましたが、それは目に見えず、どこにも言及されていません
Aconcagua

1

C ++で使用できる少なくとも2つのオプションがありません。

1つは、依存関係がテンプレートパラメータである「静的な」依存性注入を使用することです。これにより、コンパイル時の依存性注入を許可しながら、値によって依存性を保持するオプションが提供されます。STLコンテナーは、たとえば、アロケーターと比較およびハッシュ関数にこのアプローチを使用します。

もう1つの方法は、ディープコピーを使用して値によってポリモーフィックオブジェクトを取得することです。これを行う従来の方法は、仮想クローンメソッドを使用することです。普及しているもう1つのオプションは、型消去を使用して、多態的に動作する値型を作成することです。

どのオプションが最も適切かは、実際の使用例によって異なりますが、一般的な答えを出すのは困難です。静的なポリモーフィズムのみが必要な場合でも、テンプレートが最もC ++の方法です。


0

あなたが投稿した最初のコード(値によってメンバーを保存する「古き良き時代」)と依存性注入を組み合わせた4番目の可能な答えを無視しました:

class Foobar {
    Widget widget;
public:
    Foobar(Widget w) // pass w by value
     : widget{ std::move(w) } {}
};

クライアントコードは次のように記述できます。

Widget w;
Foobar f1{ w }; // default: copy w into f1
Foobar f2{ std::move(w) }; // move w into f2

オブジェクト間の結合を表すことは、リストした基準に基づいて(純粋に)行われるべきではありません(つまり、「安全なライフタイム管理に適している」だけに基づいているわけではありません)。

概念的な基準を使用することもできます(「車には4つの車輪があります」対「車は運転者が持ち込む必要がある4つの車輪を使用します」)。

他のAPIによって条件を課すことができます(たとえば、APIから取得するものがカスタムラッパーまたはstd :: unique_ptrである場合、クライアントコードのオプションも制限されます)。


1
これにより、付加価値が大幅に制限される多態的な動作が排除されます。
-el.pescado

本当です。... -それは私がほとんどを使用して終了フォームですので、私はちょうど(私が多型の行動を必要とする多くのケースを持っていないので、ないコードの再利用私は私の多型の挙動をコードで継承を使用)、それを言及すると思った
utnapistim
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.