web-dev-qa-db-ja.com

オブジェクトの寿命の不変対移動のセマンティクス

私がずっと前に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__が常に有効なオブジェクトを指すことです。これは、デフォルトのctorがなく、コピーではない唯一のctorだからです。俳優はこれらを保証します。すべてのoperator()メソッドは、この不変条件が成立することを前提とし、後でそれを保持します。

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

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

仮に私がこのクラスを書いたとしましょう。数か月後、架空の同僚がUBを経験したのは、これらのオブジェクトの多くが何らかの理由でシャッフルされている複雑な関数で、彼がこれらのいずれかから移動し、後でそのメソッド。明らかにそれは結局のところ彼のせいですが、このクラスは「不十分な設計」ですか?

考え:

  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. クラスをコピー可能にした方がよいのですが、移動はできませんか?
    これも悪いように見え、パフォーマンスに影響を与えますが、「不変」の問題を簡単に解決します。

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

13
Chris Beck

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

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

次の関数を考えてみましょう:

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

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

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

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

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

つまり、any前提条件を持つ関数を呼び出すことはできません。 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から例外をスローします。一部のメソッドで不変条件を保持できない場合は、なんらかの方法でエラーを通知してロールバックします。これは、移動元のオブジェクトで異なる必要がありますか?

Moveコンストラクターから例外をスローすることは、一般的に考えられています...失礼です。メモリを所有するオブジェクトを移動すると、そのメモリの所有権が移ります。そして、それは通常投げることができる何かを含みません。

悲しいことに さまざまな理由でこれを強制することはできません 。スロー移動は可能性があることを受け入れる必要があります。

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

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

覚えておいてください:動きはパフォーマンスの最適化であり、通常はabout破壊されます。このコードを考えてみましょう:

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

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

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

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

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

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

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

クラスをコピー可能にした方がよいのですが、移動はできませんか?

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

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

20
Nicol Bolas