web-dev-qa-db-ja.com

関数の戻り値でstd :: moveを使用する場合

この場合

struct Foo {};
Foo meh() {
  return std::move(Foo());
}

新しく作成されたFooはxvalueになるため、移動は不要であると確信しています。

しかし、このような場合はどうでしょうか?

struct Foo {};
Foo meh() {
  Foo foo;
  //do something, but knowing that foo can safely be disposed of
  //but does the compiler necessarily know it?
  //we may have references/pointers to foo. how could the compiler know?
  return std::move(foo); //so here the move is needed, right?
}

そこで移動が必要だと思う?

108
user2015453

return std::move(foo);の場合、12.8/32のためmoveは不要です。

ソースオブジェクトが関数パラメーターであり、コピーされるオブジェクトが左辺値で指定されているという事実を除いて、コピー操作の省略の基準が満たされるか満たされる場合、コピーのコンストラクターを選択するためのオーバーロード解決はオブジェクトが右辺値で指定されているかのように最初に実行されました。

return foo;はNRVOのケースであるため、コピーの省略は許可されています。 fooは左辺値です。したがって、fooからmehの戻り値への「コピー」用に選択されたコンストラクターは、移動コンストラクター(存在する場合)である必要があります。

moveを追加すると潜在的な効果がありますが、return std::move(foo);not NRVOに適格であるため、移動を回避できません。

私の知る限り、12.8/32ではonlyの条件で、左辺値からのコピーを移動に置き換えることができます。コンパイラーは一般に、コピー後に左辺値が未使用であることを検出することは許可されておらず(たとえば、DFAを使用)、独自のイニシアチブで変更を行います。ここでは、2つの間に観察可能な違いがあると仮定しています。観察可能な動作が同じ場合、「as-if」ルールが適用されます。

そのため、タイトル内の質問に答えるには、移動したい場合に戻り値にstd::moveを使用し、それでも移動しないようにします。あれは:

  • あなたはそれを動かしたい、そして
  • それは左辺値であり、
  • コピー省略の対象ではありません。
  • 値渡しの関数パラメーターの名前ではありません。

これは非常に手間がかかり、動きが通常安いことを考えると、非テンプレートコードではこれを少し単純化できると言うことができます。 std::moveは次の場合に使用します。

  • あなたはそれを動かしたい、そして
  • それは左辺値であり、
  • あなたはそれを心配することはできません。

簡素化されたルールに従うことにより、移動の省略を犠牲にします。 std::vectorのような移動が安価なタイプの場合、おそらく気付くことはないでしょう(気付いた場合は最適化できます)。移動コストが高いstd::arrayなどのタイプ、または移動が安上がりかどうかわからないテンプレートの場合、心配することが多くなります。

112
Steve Jessop

どちらの場合も移動は不要です。 2番目の場合、ローカル変数を値で返すため、std::moveは不要です。コンパイラは、そのローカル変数をもう使用しないので、コピーするのではなく移動できることを理解します。

33
Andy Prowl

戻り値で、戻り式がローカル左辺値の名前を直接参照する場合(つまり、この時点でxvalue)、std::moveの必要はありません。一方、戻り式がnot識別子の場合、自動的に移動されないため、たとえば、この場合は明示的なstd::moveが必要になります。

T foo(bool which) {
   T a = ..., b = ...;
   return std::move(which? a : b);
   // alternatively: return which? std::move(a), std::move(b);
}

名前付きローカル変数または一時式を直接返す場合、明示的なstd::moveを避ける必要があります。これらの場合、コンパイラーmust(および今後)は自動的に移動し、std::moveを追加すると他の最適化に影響する可能性があります。

いつ移動すべきでないかについては多くの回答がありますが、質問は「いつ移動すべきか」です。

これは、使用する場合の不自然な例です。

std::vector<int> append(std::vector<int>&& v, int x) {
  v.Push_back(x);
  return std::move(v);
}

すなわち、右辺値参照を受け取り、それを変更し、そのコピーを返す関数がある場合。現在、実際には、この設計はほぼ常に優れています。

std::vector<int> append(std::vector<int> v, int x) {
  v.Push_back(x);
  return v;
}

また、非右辺値パラメーターを使用することもできます。

基本的に、移動して返す関数内に右辺値参照がある場合は、std::moveを呼び出す必要があります。ローカル変数がある場合(パラメーターであるかどうかに関係なく)、暗黙的にmovesを返します(この暗黙的な移動は省略できますが、明示的な移動はできません)。ローカル変数を受け取り、そのローカル変数への参照を返す関数または操作がある場合は、std::moveを使用して移動を発生させる必要があります(例として、三項?:演算子)。

C++コンパイラはstd::move(foo)を自由に使用できます:

  • fooがライフタイムの終わりにあることがわかっている場合、および
  • std::moveの暗黙的な使用は、C++仕様で許可されているセマンティック効果以外に、C++コードのセマンティクスに影響を与えません。

f(foo); foo.~Foo();からf(std::move(foo)); foo.~Foo();へのどの変換が、パフォーマンスの観点から、またはメモリ消費の観点から有益であるかを計算できるかどうかは、C++コンパイラの最適化機能に依存します。 。


概念的に言えば、GCC 6.3.0などの2017年C++コンパイラは最適化可能このコード:

Foo meh() {
    Foo foo(args);
    foo.method(xyz);
    bar();
    return foo;
}

このコードに:

void meh(Foo *retval) {
   new (retval) Foo(arg);
   retval->method(xyz);
   bar();
}

Fooのコピーコンストラクターとデストラクターを呼び出さないようにします。


GCC 6.3.0などの2017年のC++コンパイラは、最適化できませんこれらのコードです:

Foo meh_value() {
    Foo foo(args);
    Foo retval(foo);
    return retval;
}

Foo meh_pointer() {
    Foo *foo = get_foo();
    Foo retval(*foo);
    delete foo;
    return retval;
}

これらのコードに:

Foo meh_value() {
    Foo foo(args);
    Foo retval(std::move(foo));
    return retval;
}

Foo meh_pointer() {
    Foo *foo = get_foo();
    Foo retval(std::move(*foo));
    delete foo;
    return retval;
}

つまり、2017年のプログラマーはそのような最適化を明示的に指定する必要があります。

1
atomsymbol