web-dev-qa-db-ja.com

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

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

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を結合します。解決策は一見単純なようです:Dependency Injectionを適用してFoobarクラスから依存関係を構築しますが、C++では依存関係の所有権について考えるように強制します。3つの解決策が思い浮かびます:

ユニークなポインター

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

Foobarは、渡されたWidgetの唯一の所有権を主張します。これには次の利点があります。

  1. パフォーマンスへの影響は無視できます。
  2. Foobarはライフタイムodその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) {}
    (…)
}

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

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

短所:

  1. 共有が関与していないときにリソースを浪費します。
  2. それでもヒープの割り当てが必要で、スタックに割り当てられたオブジェクトは許可されません。

プレーンOL '監視ポインタ

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

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

  1. それが得ることができるのと同じくらい簡単です。
  2. ユニバーサル、Widgetのみを受け入れます。

短所:

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

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

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

私は3番目のソリューションに傾いています。それは最も一般的であるため、とにかくFoobarsを管理する必要があるため、Widgetsの管理は単純な変更です。ただし、生のポインタを使用すると煩わしくなります。一方、スマートポインタソリューションは、依存関係のコンシューマに依存関係の作成方法を制限させるので、私には間違っていると感じます。

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

17
el.pescado

参照の形で監視ポインタを使用します。それを使用すると、より優れた構文が得られ、プレーンポインターが可能にする所有権を意味しないという意味的な利点があります。

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

更新

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

2

私はこれをコメントとして書くつもりでしたが、結局長すぎました。

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

_std::unique_ptr<Widget>_を使用するか_std::shared_ptr<Widget>_を使用するかは、ユーザーが決定し、機能によって決まります。

Foobarなどのブロックの作成を担当する_Utilities::Factory_があるとします。 DIの原則に従って、Widgetインスタンスを必要とします。これは、Foobarのコンストラクターを使用してインスタンスを注入します。つまり、_Utilities::Factory_のメソッドの1つ、たとえばcreateWidget(const std::vector<std::string>& params) 、ウィジェットを作成してFoobarオブジェクトに挿入します。

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


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

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

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

_std::shared_ptr<Widget>_はその方法です。

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

そこで、_std::unique_ptr<Widget>_が王位を獲得します。


更新:

生涯の問題については@ DominicMcDonnellには本当に同意しません。 _std::move_で_std::unique_ptr_を呼び出すと所有権が完全に転送されるため、メソッドで_object A_を作成し、それを依存関係として別の_object B_に渡しても、_object B_は_object A_のリソースを担当し、_object B_がスコープ外になった場合に正しく削除します。

2
Andy

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

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

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

C++では、Javaには存在しない「奇妙な」ことを行うこともできます。 g。可変テンプレートコンストラクターがあります(Javaには...表記があり、コンストラクターでも使用できますが、これはオブジェクト配列を非表示にする構文上の砂糖であり、実際には可変テンプレートとは何の関係もありません! )-カテゴリ「クレイジーテンプレートメタプログラミング」:

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);

いいね?

もちろん、両方のクラスを分離したい、または分離する必要があるある理由があります-e。 g。 FooBarインスタンスが存在するはるか前にウィジェットを作成する必要がある場合、ウィジェットを再利用する必要がある場合、または単に現在の問題の場合はそれがより適切であるため(たとえば、ウィジェットがGUI要素の場合) FooBarは1つにする必要があります。

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

1
Aconcagua

C++で使用できるオプションが少なくとも2つ不足しています。

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

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

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

1
mattnewport

あなたが投稿した最初のコード(値によってメンバーを保存する「古き良き時代」)と依存関係の注入を組み合わせた、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つの車輪がある」vs「車には4つの車輪があり、ドライバーが持ち込む必要がある」)。

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

0
utnapistim