オブジェクトライフタイム不変式と移動セマンティクス


13

ずっと前にC ++を学んだとき、C ++のポイントの一部は、ループに「ループ不変式」があるように、クラスにもオブジェクトの存続期間に関連する不変式があることを強く強調しました。オブジェクトが生きている限り。コンストラクターによって確立され、メソッドによって保持されるべきもの。カプセル化/アクセス制御は、不変条件を強制するのに役立ちます。RAIIは、このアイデアでできることの1つです。

C ++ 11以降、ムーブセマンティクスがあります。移動をサポートするクラスの場合、オブジェクトからの移動はその寿命を正式に終了することはありません-移動は「有効な」状態のままにすることになっています。

クラスの設計において、クラスの不変式が移動元までしか保持されないように設計するのは悪い習慣ですか?または、高速化できるようになれば大丈夫です。

具体的にするために、コピー不可だが移動可能なリソースタイプがあるとします。

class opaque {
  opaque(const opaque &) = delete;

public:
  opaque(opaque &&);

  ...

  void mysterious();
  void mysterious(int);
  void mysterious(std::vector<std::string>);
};

そして、何らかの理由で、おそらく既存のディスパッチシステムで使用できるように、このオブジェクトのコピー可能なラッパーを作成する必要があります。

class copyable_opaque {
  std::shared_ptr<opaque> o_;

  copyable_opaque() = delete;
public:
  explicit copyable_opaque(opaque _o)
    : o_(std::make_shared<opaque>(std::move(_o)))
  {}

  void operator()() { o_->mysterious(); }
  void operator()(int i) { o_->mysterious(i); }
  void operator()(std::vector<std::string> v) { o_->mysterious(v); }
};

このcopyable_opaqueオブジェクトでは、構築時に確立されるクラスの不変式は、o_デフォルトのアクタがなく、コピーアクタではない唯一のアクタがこれらを保証するため、メンバは常に有効なオブジェクトを指すことです。すべてのoperator()メソッドは、この不変式が保持されることを前提とし、後で保持します。

ただし、オブジェクトの移動元の場合は、o_何も指し示しません。そして、その後、いずれかのメソッドoperator()を呼び出すと、UB /クラッシュが発生します。

オブジェクトが決して移動されない場合、不変式はdtor呼び出しまで保持されます。

仮に、このクラスを書いて、数ヶ月後に、私の想像上の同僚がUBを体験したとしましょう。そのメソッド。明らかに、それは1日の終わりに彼のせいですが、このクラスは「不十分に設計されていますか」

考え:

  1. 通常、C ++では、触れると爆発するゾンビオブジェクトを作成するのは不適切です。
    オブジェクトを構築できない場合、不変式を確立できず、その後、ctorから例外をスローします。何らかの方法で不変式を保持できない場合は、何らかの方法でエラーを通知し、ロールバックします。これは、移動元オブジェクトと異なる必要がありますか?

  2. 「このオブジェクトが移動された後、それを破壊する以外に何かを行うことは違法(UB)です」とヘッダーに文書化するだけで十分ですか?

  3. 各メソッド呼び出しで有効であると継続的にアサートする方が良いですか?

そのようです:

class copyable_opaque {
  std::shared_ptr<opaque> o_;

  copyable_opaque() = delete;
public:
  explicit copyable_opaque(opaque _o)
    : o_(std::make_shared<opaque>(std::move(_o)))
  {}

  void operator()() { assert(o_); o_->mysterious(); }
  void operator()(int i) { assert(o_); o_->mysterious(i); }
  void operator()(std::vector<std::string> v) { assert(o_); o_->mysterious(v); }
};

アサーションは動作を実質的に改善せず、スローダウンを引き起こします。プロジェクトが常にアサーションで実行されるのではなく、「リリースビルド/デバッグビルド」スキームを使用する場合、リリースビルドのチェックにお金を払わないため、これはより魅力的だと思います。デバッグビルドが実際にない場合、これはかなり魅力的ではないように見えます。

  1. クラスをコピー可能にするのが良いのですが、移動できないようにしますか?
    これも悪いように見え、パフォーマンスに影響を与えますが、「不変」問題を簡単に解決します。

ここで関連する「ベストプラクティス」は何だと思いますか?



回答:


20

通常、C ++では、触れると爆発するゾンビオブジェクトを作成するのは不適切です。

しかし、それはあなたがしていることではありません。間違ってタッチすると爆発する「ゾンビオブジェクト」を作成しています。最終的には、他の状態ベースの前提条件と違いはありません。

次の機能を検討してください。

void func(std::vector<int> &v)
{
  v[0] = 5;
}

この機能は安全ですか?番号; ユーザーは空の を渡すことができますvector。そのため、関数にはv少なくとも1つの要素が含まれる事実上の前提条件があります。そうでない場合、を呼び出すとUBを取得しますfunc

したがって、この関数は「安全」ではありません。しかし、それは壊れているという意味ではありません。それを使用するコードが前提条件に違反している場合にのみ破損します。たぶんfunc、他の関数の実装のヘルパーとして使用される静的関数です。そのようにローカライズされた、誰もその前提条件に違反する方法でそれを呼び出すことはありません。

名前空間スコープであろうとクラスメンバであろうと、多くの関数は、操作対象の値の状態に期待を持っています。これらの前提条件が満たされていない場合、通常はUBで機能が失敗します。

C ++標準ライブラリは、「valid-but-unspecified」ルールを定義します。これは、標準で特に指定されていない限り、移動元のオブジェクトはすべて有効であることを示します(そのタイプの正当なオブジェクトです)が、そのオブジェクトの特定の状態は指定されていません。移動元にはvectorいくつの要素がありますか?それは言いません。

あなたが持っている任意の関数を呼び出すことができない。この意味任意の前提条件を。vector::operator[]にはvector少なくとも1つの要素があるという前提条件があります。の状態がわからないため、vector呼び出すことはできません。が空でないfuncことを最初に確認せずに呼び出すよりも良いでしょうvector

ただし、これは、前提条件を持たない関数が適切であることも意味します。これは完全に正当なC ++ 11コードです。

vector<int> v1 = {1, 2, 3, 4, 5};
vector<int> v2{std::move(v1)};
v1.assign({6, 7, 8, 9, 10});

vector::assign前提条件はありません。有効なvectorオブジェクト(移動元のオブジェクトも含む)で動作します。

したがって、壊れたオブジェクトを作成しているわけではありません。状態が不明なオブジェクトを作成しています。

オブジェクトを構築できない場合、不変条件を確立できず、その後、ctorから例外をスローします。何らかの方法で不変式を保持できない場合は、何らかの方法でエラーを通知してロールバックします。これは、移動元のオブジェクトと異なる必要がありますか?

移動コンストラクターから例外をスローすることは、一般に無礼と見なされます。メモリを所有するオブジェクトを移動する場合、そのメモリの所有権を譲渡します。そして、それは通常、投げることができるものには関係しません。

悲しいことに、さまざまな理由でこれを強制することはできません。スロームーブは可能性であることを受け入れなければなりません。

また、「まだ指定されていない」言語に従う必要がないことにも注意してください。これは、C ++標準ライブラリが標準型の移動がデフォルトで機能することを示す方法です。特定の標準ライブラリタイプには、より厳密な保証があります。たとえばunique_ptr、移動元unique_ptrインスタンスの状態については非常に明確nullptrです。それはに等しいです。

したがって、必要に応じて、より強力な保証を提供することを選択できます。

ただ覚えている:動きがあるパフォーマンスの最適化、通常されているオブジェクトに対して行われている1 程度に破壊します。次のコードを検討してください。

vector<int> func()
{
  vector<int> v;
  //fill up `v`.
  return v;
}

これはv戻り値に移動します(コンパイラーがそれを省略しないと仮定)。また、移動が完了した後に参照する方法はありませんv。したがってv、有用な状態にするために行った作業は無意味です。

ほとんどのコードでは、移動元オブジェクトインスタンスを使用する可能性は低くなります。

「このオブジェクトが移動された後、それを破壊する以外に何かを行うことは違法(UB)です」とヘッダーに文書化するだけで十分ですか?

各メソッド呼び出しで有効であると継続的にアサートする方が良いですか?

前提条件を持つことの全体的なポイントは、そのようなことをチェックしないことです。operator[]にはvector、指定されたインデックスを持つ要素があるという前提条件があります。のサイズの外側にアクセスしようとすると、UBを取得しますvector。そのような前提条件vector::at はありません。にvectorそのような値がない場合、明示的に例外をスローします。

パフォーマンス上の理由から前提条件が存在します。これらは、呼び出し側が自分で確認できたものをチェックする必要がないようにするためです。を呼び出すたびに、空v[0]かどうかを確認する必要vはありません。最初のものだけが行います。

クラスをコピー可能にするのが良いのですが、移動できないようにしますか?

いいえ。実際、クラスは決して「コピー可能だが移動できない」べきでありません。コピーできる場合は、コピーコンストラクターを呼び出して移動できる必要があります。これは、ユーザー定義のコピーコンストラクターを宣言するが、移動コンストラクターを宣言しない場合のC ++ 11の標準的な動作です。そして、特別な移動セマンティクスを実装したくない場合に採用すべき動作です。

移動セマンティクスは、非常に特定の問題を解決するために存在します。コピーが非常に高価または無意味である大きなリソース(つまり、ファイルハンドル)を持つオブジェクトを扱うことです。オブジェクトが適格でない場合、コピーと移動は同じです。


5
いいね +1。「前提条件を持つことの全体的なポイントは、そのようなことをチェックしないことです。」-私はこれが主張に当てはまるとは思わない。アサーションは私見チェック前提条件に優れた、有効なツールである(少なくともほとんどの時間。)
マーティンBaの

3
コピー/移動の混乱は、移動アクタがソースオブジェクトを新しいオブジェクトと同一の状態を含む任意の状態のままにする可能性があることを理解することによって明らかにすることができます。
–MSalters
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.