web-dev-qa-db-ja.com

x = std :: move(x)は未定義ですか?

xを、以前に初期化されたあるタイプの変数とします。次の行です:

x = std::move(x)

未定義?これは標準のどこにあり、それについて何と言っていますか?

61
becko

いいえ、これは未定義の動作ではありません。実装で定義された動作になります。移動の割り当てがどのように実装されているかによって異なります。

これに関連するのは LWG問題2468:ライブラリタイプの自己移動-割り当て 、これはアクティブな問題であり、公式の提案がないため、これは決定的なものではなく指標と見なす必要がありますが、標準ライブラリに関係するセクションを指摘し、それらが現在競合していることを指摘します。それは言う:

私たちが書くとしましょう

vector<string> v{"a", "b", "c", "d"};
v = move(v);

Vの状態はどうあるべきですか?標準では、自己移動割り当てについて具体的なことは何も述べられていません。標準のいくつかの部分に関連するテキストがあり、それらを調整する方法が明確ではありません。

[...]

どちらが優先されるかが明確でないため、これらの部分をどのように組み合わせるかはテキストからは明確ではありません。多分17.6.4.9 [res.on.arguments]が勝ちます(MoveAssignable要件に記載されていない暗黙の前提条件を課すため、v = move(v)は未定義です)、または多分23.2.1 [container.requirements.general ] wins(Container :: operator =に対して、一般的なライブラリ関数で保証されている以上の追加の保証を明示的に提供するため、v = move(v)は何もしません)、または他の何か。

私がチェックした既存の実装では、その価値については、v = move(v)がベクトルをクリアするように見えました。ベクトルを変更せず、クラッシュを引き起こしませんでした。

そして提案します:

非公式:MoveAssignableおよびContainer要件テーブル(および移動割り当てについて言及しているその他の要件テーブル)を変更して、x = move(x)が定義された動作であり、xを有効であるが指定されていない状態のままにすることを明示します。それはおそらく今日の標準が言っていることではありませんが、おそらく私たちが意図したものであり、ユーザーに伝えたことや実装が実際に行っていることと一致しています。

組み込み型の場合、これは基本的にコピーであることに注意してください。ドラフトC++ 14標準セクションから5.17[expr.ass]

単純な割り当て(=)では、式の値は、左のオペランドによって参照されるオブジェクトの値を置き換えます。

これは、5.17が言うクラスの場合とは異なります。

左側のオペランドがクラスタイプの場合、クラスは完全でなければなりません。クラスのオブジェクトへの割り当ては、コピー/ムーブ代入演算子(12.8、13.5.3)によって定義されます。

Clangには 自己移動警告 があることに注意してください:

ログ:新しい警告-Wself-moveをClangに追加します。

-Wself-moveは-Wself-assignに似ています。この警告は、値をそれ自体に移動しようとしたときにトリガーされます。この警告で検出されたバグについては、r221008を参照してください。

53
Shafik Yaghmour

X::operator = (X&&)を呼び出すため、このケースを管理するのは実装次第です(X::operator = (const X&)の場合と同様)。

13
Jarod42

行うのはX::operator=(X&&)を呼び出すことだけです(左辺値修飾 "_*this_"を使用)。

プリミティブ型では、_std::move_はほとんど関心がなく、_=_とはまったく相互作用しません。したがって、これはクラスタイプのオブジェクトにのみ適用されます。

現在、std内のタイプ(またはそのテンプレートの1つによって生成されたタイプ)の場合、movedのオブジェクトは、指定されていない(まだ有効な)状態のままになる傾向があります。これは未定義の動作ではありませんが、有用な動作ではありません。

与えられたそれぞれのX::operator=(X&&)のセマンティクスを調べる必要があります。std内のすべての型を調べると、スタックオーバーフローの答えとして「広すぎる」ことになります。彼らは彼ら自身と矛盾するかもしれません。

一般に、オブジェクトからmoveするときは、「オブジェクトがその後どのような状態になるかは気にしない」とコンシューマーに伝えます。したがって、x = std::move(x)の使用は失礼です。これは、(通常)do操作の完了後にxがどのような状態にあるかを気にするためです(割り当てている場合)。 )。同じ操作で同じオブジェクトを左辺値と右辺値の両方として使用していますが、これは良い方法ではありません。

興味深い例外は、デフォルトの_std::swap_で、次のようになります。

_template<class T>
void swap(T& lhs, T& rhs) {
  T tmp = std::move(lhs);
  lhs = std::move(rhs);
  rhs = std::move(tmp);
}
_

真ん中の行lhs = std::move(rhs)は、同じオブジェクトでswapを2回呼び出すと、x = std::move(x)を実行します。

ただし、この行が完了した後のxの状態は関係ないことに注意してください。 xの状態はすでにtmpに格納されており、次の行で復元します。