web-dev-qa-db-ja.com

C ++で不変で効率的なクラスを作成する慣用的な方法

私はこのようなことをしたいと思っています(C#)。

_public final class ImmutableClass {
    public readonly int i;
    public readonly OtherImmutableClass o;
    public readonly ReadOnlyCollection<OtherImmutableClass> r;

    public ImmutableClass(int i, OtherImmutableClass o,
        ReadOnlyCollection<OtherImmutableClass> r) : i(i), o(o), r(r) {}
}
_

私が遭遇した潜在的な解決策とそれに関連する問題は次のとおりです。

1。クラスメンバーconstを使用しますが、これはデフォルトのコピー割り当て演算子が削除されることを意味します。

解決策1:

_struct OtherImmutableObject {
    const int i1;
    const int i2;

    OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
}
_

問題1:

_OtherImmutableObject o1(1,2);
OtherImmutableObject o2(2,3);
o1 = o2; // error: use of deleted function 'OtherImmutableObject& OtherImmutableObject::operator=(const OtherImmutableObject&)`
_

EDIT:不変オブジェクトを_std::vector_に格納したいがerror: use of deleted function 'OtherImmutableObject& OtherImmutableObject::operator=(OtherImmutableObject&&)を受け取るため、これは重要です

2。 getメソッドを使用して値を返すが、これは、大きなオブジェクトをコピーする必要があることを意味します。これは、回避する方法を知りたい非効率です。 このスレッド はgetソリューションを提案しますが、元のオブジェクトをコピーせずに非プリミティブオブジェクトの受け渡しを処理する方法は扱いません。

解決策2:

_class OtherImmutableObject {
    int i1;
    int i2;
public:
    OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
    int GetI1() { return i1; }
    int GetI2() { return i2; }
}

class ImmutableObject {
    int i1;
    OtherImmutableObject o;
    std::vector<OtherImmutableObject> v;
public:
    ImmutableObject(int i1, OtherImmutableObject o,
        std::vector<OtherImmutableObject> v) : i1(i1), o(o), v(v) {}
    int GetI1() { return i1; }
    OtherImmutableObject GetO() { return o; } // Copies a value that should be immutable and therefore able to be safely used elsewhere.
    std::vector<OtherImmutableObject> GetV() { return v; } // Copies the vector.
}
_

問題2:不要なコピーは非効率的です。

3。 getメソッドを使用してconst参照またはconstポインタを返すが、これにより、ハングした参照またはポインタが残る可能性がある。 このスレッド は、関数の戻りからスコープ外に出る参照の危険性について話します。

解決策3:

_class OtherImmutableObject {
    int i1;
    int i2;
public:
    OtherImmutableObject(int i1, int i2) : i1(i1), i2(i2) {}
    int GetI1() { return i1; }
    int GetI2() { return i2; }
}

class ImmutableObject {
    int i1;
    OtherImmutableObject o;
    std::vector<OtherImmutableObject> v;
public:
    ImmutableObject(int i1, OtherImmutableObject o,
        std::vector<OtherImmutableObject> v) : i1(i1), o(o), v(v) {}
    int GetI1() { return i1; }
    const OtherImmutableObject& GetO() { return o; }
    const std::vector<OtherImmutableObject>& GetV() { return v; }
}
_

問題3:

_ImmutableObject immutable_object(1,o,v);
// elsewhere in code...
OtherImmutableObject& other_immutable_object = immutable_object.GetO();
// Somewhere else immutable_object goes out of scope, but not other_immutable_object
// ...and then...
other_immutable_object.GetI1();
// The previous line is undefined behaviour as immutable_object.o will have been deleted with immutable_object going out of scope
_

Getメソッドのいずれかから参照を返すと、未定義の動作が発生する可能性があります。

37
lachy
  1. あるタイプと値のセマンティクスの不変オブジェクトが本当に必要な場合(ランタイムパフォーマンスに関心があり、ヒープを避けたい場合)。すべてのデータメンバーstructpublicを定義するだけです。

    struct Immutable {
        const std::string str;
        const int i;
    };
    

    それらをインスタンス化してコピーし、データメンバーを読み取ることができますが、それだけです。別のインスタンスの右辺値参照からインスタンスを移動構築しても、まだコピーされます。

    Immutable obj1{"...", 42};
    Immutable obj2 = obj1;
    Immutable obj3 = std::move(obj1); // Copies, too
    
    obj3 = obj2; // Error, cannot assign
    

    このようにして、クラスのすべての使用が不変性を尊重することを確認します(だれも悪いことをしないと仮定してconst_castのもの)。追加の機能は無料の関数を通じて提供できます。データ関数の読み取り専用の集計にメンバー関数を追加しても意味がありません。

  2. 値のセマンティクスはそのままで1.が少し緩和され(オブジェクトが実際には不変ではなくなるなど)、実行時のパフォーマンスのためにmove-constructionが必要であることも懸念しています。 privateデータメンバーとゲッターメンバー関数を回避する方法はありません。

    class Immutable {
       public:
          Immutable(std::string str, int i) : str{std::move(str)}, i{i} {}
    
          const std::string& getStr() const { return str; }
          int getI() const { return i; }
    
       private:
          std::string str;
          int i;
    };
    

    使い方は同じですが、move構文は実際に動きます。

    Immutable obj1{"...", 42};
    Immutable obj2 = obj1;
    Immutable obj3 = std::move(obj1); // Ok, does move-construct members
    

    割り当てを許可するかどうかは、現在あなたの管理下にあります。ただ= delete不要な場合は代入演算子を使用します。それ以外の場合は、コンパイラが生成した演算子を使用するか、独自に実装します。

    obj3 = obj2; // Ok if not manually disabled
    
  3. シナリオでは、値のセマンティクスやアトミック参照カウントの増分が問題ないことは気にしません。 @ NathanOliver's answer に示されているソリューションを使用します。

33
lubgr

基本的には、std::unique_ptrまたはstd::shared_ptrを利用することで、必要なものを取得できます。これらのオブジェクトの1つだけが必要で、移動できるようにする場合は、std::unique_ptrを使用できます。すべて同じ値を持つ複数のオブジェクト(「コピー」)を許可する場合は、std::shared_Ptrを使用できます。エイリアスを使用して名前を短くし、ファクトリ関数を提供すると、かなり簡単になります。これにより、コードは次のようになります。

class ImmutableClassImpl {
public: 
    const int i;
    const OtherImmutableClass o;
    const ReadOnlyCollection<OtherImmutableClass> r;

    public ImmutableClassImpl(int i, OtherImmutableClass o, 
        ReadOnlyCollection<OtherImmutableClass> r) : i(i), o(o), r(r) {}
}

using Immutable = std::unique_ptr<ImmutableClassImpl>;

template<typename... Args>
Immutable make_immutable(Args&&... args)
{
    return std::make_unique<ImmutableClassImpl>(std::forward<Args>(args)...);
}

int main()
{
    auto first = make_immutable(...);
    // first points to a unique object now
    // can be accessed like
    std::cout << first->i;
    auto second = make_immutable(...);
    // now we have another object that is separate from first
    // we can't do
    // second = first;
    // but we can transfer like
    second = std::move(first);
    // which leaves first in an empty state where you can give it a new object to point to
}

代わりにshared_ptrを使用するようにコードを変更する場合は、次のようにすることができます

second = first;

そして、両方のオブジェクトが同じオブジェクトを指していますが、どちらもそれを変更することはできません。

22
NathanOliver

C++の普遍的な値のセマンティクスのため、C++の不変性を他のほとんどの一般的な言語の不変性と直接比較することはできません。 「不変」の意味を理解する必要があります。

タイプOtherImmutableObjectの変数に新しい値を割り当てられるようにしたい。 C#ではImmutableObject型の変数を使用して実行できるため、これは理にかなっています。

その場合、必要なセマンティクスを取得する最も簡単な方法は

struct OtherImmutableObject {
    int i1;
    int i2;
};

これは変更可能であるように見えるかもしれません。結局のところ、あなたは書くことができます

OtherImmutableObject x{1, 2};
x.i1 = 3;

しかし、その2行目の効果は(同時実行を無視...)の効果とまったく同じです。

x = OtherImmutableObject{3, x.i2};

したがって、OtherImmutableObject型の変数への割り当てを許可する場合は、追加のセマンティック保証が提供されないため、メンバーへの直接割り当てを禁止しても意味がありません。同じ抽象操作のコードを遅くするだけです。 (この場合、ほとんどの最適化コンパイラーはおそらく両方の式に対して同じコードを生成しますが、メンバーの1つがstd::stringであった場合、それを行うには十分に賢くないかもしれません。)

これは、intstd::complexstd::stringなどを含む、C++の基本的にすべての標準型の動作です。新しい値を割り当てることができるという意味で、これらはすべて変更可能です。それらを変更するために(抽象的に)実行できる唯一のことは、C#の不変の参照型と同様に、新しい値を割り当てることです。

そのセマンティクスが必要ない場合、他の唯一のオプションは割り当てを禁止することです。型のすべてのメンバーをconstと宣言するのではなく、変数をconstと宣言することでそれを行うことをお勧めします。これにより、クラスの使用方法のオプションが増えるためです。たとえば、最初に変更可能なクラスのインスタンスを作成し、その中に値を作成し、その後constStringBuilderstring、ただしそれをコピーするオーバーヘッドはありません。

(すべてのメンバーをconstと宣言する1つの考えられる理由は、場合によってはより良い最適化を可能にすることです。たとえば、関数がOtherImmutableObject const&を取得し、コンパイラーがコンパイラーを認識できない場合呼び出しサイトでは、基になるオブジェクトにconst修飾子がない可能性があるため、他の不明なコードへの呼び出し間でメンバーの値をキャッシュすることは安全ではありません。しかし、実際のメンバーがconstと宣言されている場合、それから値をキャッシュしても安全だと思います。)

12
benrg

あなたの質問に答えるために、オブジェクト全体へのconsting参照がトリックをするので、C++で不変のデータ構造を作成しません。ルールの違反は、const_castsの存在によって可視化されます。

Kevlin Henneyの「同期象限の外側で考える」を参照する場合、データについて2つの質問があります。

  • 構造は不変ですか?
  • 共有されていますか、共有されていませんか?

これらの質問は、4象限のニース2x2テーブルに配置できます。同時実行コンテキストでは、同期が必要なのは1つの象限、つまり変更可能な共有データのみです。

実際、不変データは書き込みができないため同期する必要はなく、同時読み取りも問題ありません。非共有データは、データの所有者だけがデータに書き込みまたは読み取りできるため、同期する必要はありません。

したがって、データ構造が非共有コンテキストで変更可能であることは問題なく、不変性の利点は共有コンテキストでのみ発生します。

IMO、あなたに最も自由を与える解決策は、クラスを可変性と不変性の両方について定義することです。

/* const-correct */ class C {
   int f1_;
   int f2_;

   const int f3_; // Semantic constness : initialized and never changed.
};

次に、クラスCのインスタンスを可変または不変として使用できます。どちらの場合も、constness-where-it-makes-senseのメリットがあります。

オブジェクトを共有したい場合は、constへのスマートポインターにオブジェクトをパックできます。

shared_ptr<const C> ptr = make_shared<const C>(f1, f2, f3);

この戦略を使用すると、同期の象限から安全に離れながら、3つの非同期の象限全体に自由度が広がります。 (したがって、構造を不変にする必要性を制限します)

5

最も慣用的な方法は次のようになると思います:

struct OtherImmutable {
    int i1;
    int i2;

    OtherImmutable(int i1, int i2) : i1(i1), i2(i2) {}
};

しかし...それは不変ではありませんか?

確かに、値として渡すことができます:

void frob1() {
    OtherImmutable oi;
    oi = frob2(oi);
}

auto frob2(OtherImmutable oi) -> OtherImmutable {
    // cannot affect frob1 oi, since it's a copy
}

さらに良いことに、ローカルで変更する必要がない場所では、ローカル変数をconstとして定義できます。

auto frob2(OtherImmutable const oi) -> OtherImmutable {
    return OtherImmutable{oi.i1 + 1, oi.i2};
}
4

C++には、quiteは、クラスを不変またはconstとして事前定義する機能がありません。

そして、ある時点で、おそらくC++のクラスメンバーにconstを使用してはならないという結論に達するでしょう。煩わしいだけの価値はありません。正直に言って、それなしで実行できます。

実用的な解決策として、私は試してみます:

typedef class _some_SUPER_obtuse_CLASS_NAME_PLEASE_DONT_USE_THIS { } const Immutable;

コードでImmutable以外のものを使用しないようにします。

1
user541686

不変オブジェクトは、ポインタセマンティクスでより適切に機能します。したがって、スマートな不変ポインターを作成します。

struct immu_tag_t {};
template<class T>
struct immu:std::shared_ptr<T const>
{
  using base = std::shared_ptr<T const>;

  immu():base( std::make_shared<T const>() ) {}

  template<class A0, class...Args,
    std::enable_if_t< !std::is_base_of< immu_tag_t, std::decay_t<A0> >{}, bool > = true,
    std::enable_if_t< std::is_construtible< T const, A0&&, Args&&... >{}, bool > = true
  >
  immu(A0&& a0, Args&&...args):
    base(
      std::make_shared<T const>(
        std::forward<A0>(a0), std::forward<Args>(args)...
      )
    )
  {}
  template<class A0, class...Args,
    std::enable_if_t< std::is_construtible< T const, std::initializer_list<A0>, Args&&... >{}, bool > = true
  >
  immu(std::initializer_list<A0> a0, Args&&...args):
    base(
      std::make_shared<T const>(
        a0, std::forward<Args>(args)...
      )
    )
  {}

  immu( immu_tag_t, std::shared_ptr<T const> ptr ):base(std::move(ptr)) {}
  immu(immu&&)=default;
  immu(immu const&)=default;
  immu& operator=(immu&&)=default;
  immu& operator=(immu const&)=default;

  template<class F>
  immu modify( F&& f ) const {
    std::shared_ptr<T> ptr;
    if (!*this) {
      ptr = std::make_shared<T>();
    } else {
      ptr = std::make_shared<T>(**this);
    }
    std::forward<F>(f)(*ptr);
    return {immu_tag_t{}, std::move(ptr)};
  }
};

これは、そのほとんどの実装でshared_ptrを活用しています。 shared_ptrの欠点のほとんどは、不変オブジェクトの問題ではありません。

共有ptrとは異なり、オブジェクトを直接作成することができ、デフォルトではnull以外の状態が作成されます。移動することで、引き続きnull状態になる可能性があります。次のようにして、null状態で作成できます。

immu<int> immu_null_int{ immu_tag_t{}, {} };

そしてnullでないint:

immu<int> immu_int;

または

immu<int> immu_int = 7;

modifyという便利なユーティリティメソッドを追加しました。変更すると、ラムダに渡してTの-​​mutableインスタンスが与えられ、immu<T>にパッケージ化されて返される前に変更されます。

具体的な使用法は次のようになります。

struct data;
using immu_data = immu<data>;
struct data {
  int i;
  other_immutable_class o;
  std::vector<other_immutable_class> r;
  data( int i_in, other_immutable_class o_in, std::vector<other_immutable_class> r_in ):
    i(i_in), o(std::move(o_in)), r( std::move(r_in))
  {}
};

次に、immu_dataを使用します。

メンバーにアクセスするには、->ではなく.が必要です。渡された場合は、ヌルimmu_datasを確認する必要があります。

.modifyの使用方法は次のとおりです。

immu_data a( 7, other_immutable_class{}, {} );
immu_data b = a.modify([&](auto& b){ ++b.i; b.r.emplace_back() });

これにより、値がbと等しいaが作成されます。ただし、iは1ずつインクリメントされ、other_immutable_classには追加のb.rがあります。デフォルトで作成されます)。 aは、bを作成しても変更されないことに注意してください。

上記にタイプミスがある可能性がありますが、私はデザインを使用しました。

凝ったものにしたい場合は、immuでコピーオンライトをサポートするか、一意の場合はその場で変更することができます。思ったより難しいです。

C++では、これを行う必要はありません。

class ImmutableObject {
    const int i1;
    const int i2;
}
ImmutableObject o1:
ImmutableObject o2;
o1 = o2; // Doesn't compile because immutable objects are not mutable.

不変/ constオブジェクトへの可変参照が必要な場合は、ポインター、スマートポインター、または reference_wrapper を使用します。実際にはいつでも誰でもコンテンツを変更できるクラスが必要な場合を除いて、これは不変クラスの反対です。

※もちろんC++は「いいえ」が存在しない言語です。これらのいくつかの本当に例外的な状況では、const_cast

1
Peter