web-dev-qa-db-ja.com

クラスメンバーとしてのメンバー変数の参照

私の仕事の場では、このスタイルが広く使われています:

#include <iostream>

using namespace std;

class A
{
public:
   A(int& thing) : m_thing(thing) {}
   void printit() { cout << m_thing << endl; }

protected:
   const int& m_thing; //usually would be more complex object
};


int main(int argc, char* argv[])
{
   int myint = 5;
   A myA(myint);
   myA.printit();
   return 0;
}

このイディオムを説明する名前はありますか?大きな複雑なオブジェクトをコピーする可能性のある大きなオーバーヘッドを防ぐためだと思いますか?

これは一般的に良い習慣ですか?このアプローチには落とし穴がありますか?

54
Angus Comber

このイディオムを説明する名前はありますか?

UMLでは、集約と呼ばれます。メンバーオブジェクトが参照クラスによってownedではないという点で、構成とは異なります。 C++では、参照またはポインターを使用して、2つの異なる方法で集計を実装できます。

大きな複雑なオブジェクトをコピーすることで発生する可能性のある大きなオーバーヘッドを防ぐためだと思いますか?

いいえ、これを使用するのは本当に悪い理由でしょう。集約の主な理由は、含まれているオブジェクトが含まれているオブジェクトによって所有されていないため、それらのライフタイムがバインドされていないことです。特に、参照されるオブジェクトのライフタイムは、参照するオブジェクトのライフタイムよりも長くなければなりません。それははるかに早く作成された可能性があり、コンテナーの存続期間の終了後も存続する可能性があります。それに加えて、参照されるオブジェクトの状態はクラスによって制御されませんが、外部から変更できます。参照がconstでない場合、クラスはその外部に存在するオブジェクトの状態を変更できます。

これは一般的に良い習慣ですか?このアプローチには落とし穴がありますか?

これは設計ツールです。いくつかのケースではそれは良いアイデアですが、いくつかのケースではそうではありません。最も一般的な落とし穴は、参照を保持するオブジェクトの寿命が、参照されるオブジェクトの寿命を超えてはならないことです。囲んでいるオブジェクトが参照を使用している場合after参照されているオブジェクトが破棄された場合、未定義の動作になります。一般に、集約よりも合成を優先する方が良いですが、必要な場合は他のツールと同様に優れたツールです。

コンストラクター注入による依存性注入 :class Aは、コンストラクターへの引数として依存関係を取得し、依存クラスへの参照をプライベート変数として保存します。

wikipedia に興味深い紹介があります。

const-correctnessの場合:

using T = int;

class A
{
public:
  A(const T &thing) : m_thing(thing) {}
  // ...

private:
   const T &m_thing;
};

しかし、このクラスの問題は、一時オブジェクトへの参照を受け入れることです:

T t;
A a1{t};    // this is ok, but...

A a2{T()};  // ... this is BAD.

追加することをお勧めします(少なくともC++ 11が必要です)。

class A
{
public:
  A(const T &thing) : m_thing(thing) {}
  A(const T &&) = delete;  // prevents rvalue binding
  // ...

private:
  const T &m_thing;
};

とにかく、コンストラクタを変更する場合:

class A
{
public:
  A(const T *thing) : m_thing(*thing) { assert(thing); }
  // ...

private:
   const T &m_thing;
};

テンポラリへのポインタがない であることがほぼ保証されています。

また、コンストラクターはポインターを受け取るため、Aのユーザーにとっては、渡すオブジェクトのライフタイムに注意を払う必要があることは明らかです。


やや関連するトピックは次のとおりです。

23
manlio

このイディオムを説明する名前はありますか?

この使用法に名前はありません。単に "Reference as class member"と呼ばれます。

大きな複雑なオブジェクトをコピーすることで発生する可能性のある大きなオーバーヘッドを防ぐためだと思いますか?

はい。また、あるオブジェクトのライフタイムを別のオブジェクトに関連付けるシナリオ。

これは一般的に良い習慣ですか?このアプローチには落とし穴がありますか?

使用法に依存します。言語機能の使用は、「コース用の馬の選択」のようなものです。すべての(ほとんどすべての)言語機能が存在することに注意することが重要です。これは、いくつかのシナリオで役立つからです。
参照をクラスメンバーとして使用する場合、注意すべき重要な点がいくつかあります。

  • クラスオブジェクトが存在するまで、参照されるオブジェクトが存在することを保証する必要があります。
  • コンストラクターメンバー初期化子リストでメンバーを初期化する必要があります。 lazy初期化を使用することはできません。これは、ポインタメンバーの場合に可能です。
  • コンパイラーはコピー割り当てoperator=()を生成しないため、ユーザーが自分でコピー割り当てを提供する必要があります。あなたのアクションを決定するのは面倒です=演算子は、そのような場合を取ります。したがって、基本的にクラスはnon-assignableになります。
  • 参照をNULLにしたり、他のオブジェクトを参照したりすることはできません。再装着が必要な場合、ポインターのように参照を使用することはできません。

最も実用的な目的(メンバーサイズによるメモリ使用量の増加を本当に懸念している場合を除く)には、ポインターまたは参照メンバーの代わりにメンバーインスタンスだけで十分です。これにより、参照/ポインタメンバが余分なメモリ使用量を犠牲にしてもたらす他の問題について心配する必要がなくなります。

ポインターを使用する必要がある場合は、生のポインターの代わりにスマートポインターを使用してください。それはあなたの人生をポインターでずっと楽にするでしょう。

17
Alok Save

C++は、クラス/構造体の構築を通じてオブジェクトの寿命を管理するための優れたメカニズムを提供します。これは、他の言語よりもC++の優れた機能の1つです。

Refまたはポインターを介して公開されているメンバー変数がある場合、原則としてカプセル化に違反します。このイディオムにより、クラスのコンシューマーは、Aのオブジェクトの状態を変更することができます。Aの知識や制御はありません。また、Aのオブジェクトのライフタイムを超えて、消費者がAの内部状態への参照/ポインタを保持することもできます。これは悪い設計です。代わりに、クラスをリファクタリングして共有オブジェクトへのref/pointerを保持し(所有しない)、コンストラクターを使用してこれらを設定できます(ライフタイムルールを規定します)。共有オブジェクトのクラスは、状況に応じてマルチスレッド/同時実行性をサポートするように設計できます。

1
Indy9000

メンバーの参照は通常、悪いと見なされます。それらは、メンバーポインターと比較して人生を困難にします。しかし、それは特に異常ではなく、特別な名前のイディオムやものでもありません。それは単にエイリアシングです。

1
Puppy