C ++ 11右辺値と移動の意味論の混乱(returnステートメント)


435

私は右辺値参照を理解し、C ++ 11のセマンティクスを移動しようとしています。

これらの例の違いは何ですか?それらのうちどれがベクターコピーを行わないでしょうか?

最初の例

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

2番目の例

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

3番目の例

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

50
参照によってローカル変数を返さないでください。右辺値参照は依然として参照です。
fredoverflow、2011

63
これは、例の意味の違いを理解するために明らかに意図的でしたlol
Tarantula

@FredOverflow古い質問ですが、コメントを理解するのに少し時間がかかりました。#2での問題はstd::move()、永続的な「コピー」が作成されたかどうかだったと思います。
3Dave

5
@DavidLively std::move(expression)は何も作成せず、式をxvalueにキャストするだけです。評価中にオブジェクトがコピーまたは移動されることはありませんstd::move(expression)
fredoverflow 2013

回答:


562

最初の例

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> &&rval_ref = return_vector();

最初の例は、によってキャッチされる一時ファイルを返しますrval_ref。その一時的なものは、そのrval_ref定義を超えて寿命が延び、値によってそれをキャッチしたかのように使用できます。これは次のように非常に似ています。

const std::vector<int>& rval_ref = return_vector();

ただし、私の書き直しでは、rval_refconst以外の方法で使用することはできません。

2番目の例

std::vector<int>&& return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

2番目の例では、ランタイムエラーを作成しました。 関数内のrval_ref破壊さtmpれたオブジェクトへの参照を保持するようになりました。運が良ければ、このコードはすぐにクラッシュします。

3番目の例

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

3番目の例は、最初の例とほぼ同じです。std::move上はtmp不要であり、それは、戻り値の最適化を阻害するとして、実際の性能pessimizationすることができます。

あなたがやっていることをコード化する最良の方法は:

ベストプラクティス

std::vector<int> return_vector(void)
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

つまり、C ++ 03と同じです。 tmpreturnステートメントでは、暗黙的に右辺値として扱われます。戻り値の最適化(コピーなし、移動なし)で返されるか、コンパイラーがRVOを実行できないと判断した場合は、ベクターの移動コンストラクターを使用してreturnを実行します。RVOが実行されず、返された型に移動コンストラクターがない場合のみ、コピーコンストラクターが戻りに使用されます。


64
ローカルオブジェクトを値で返す場合、コンパイラーはRVOを実行します。ローカルのタイプと関数の戻り値は同じであり、どちらもcv修飾されていません(constタイプを返しません)。RVOを阻害する可能性があるため、条件(:?)ステートメントで戻ることは避けてください。ローカルへの参照を返す他の関数でローカルをラップしないでください。ただreturn my_local;。複数のreturnステートメントは問題なく、RVOを阻害しません。
ハワードヒナント2013

27
注意点があります。ローカルオブジェクトのメンバーを返す場合、移動は明示的に行う必要があります。
ボイシー2013

5
@NoSenseEtAl:返品ラインに一時的に作成されたものはありません。 move一時的なものは作成しません。左辺値をx値にキャストし、コピーを作成せず、何も作成せず、何も破壊しません。その例は、lvalue-referenceによって戻りmove、戻り行からを削除した場合とまったく同じ状況です。どちらの方法でも、関数内のローカル変数へのぶら下がり参照があり、破壊されています。
ハワードヒナント2013

15
「複数のreturnステートメントは問題なく、RVOを阻害しません」:同じ変数を返す場合のみ。
重複排除機能2014

5
@Deduplicator:あなたは正しいです。意図したほど正確に話していませんでした。複数のreturnステートメントは、コンパイラーがRVOを禁止しない(つまり、実装が不可能になっても)ので、return式は依然として右辺値と見なされます。
ハワードヒナント2014

42

それらのどれもコピーしませんが、2番目は破壊されたベクトルを参照します。名前付き右辺値参照は、通常のコードにはほとんど存在しません。C ++ 03でコピーを作成した場合とまったく同じように作成します。

std::vector<int> return_vector()
{
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

今を除いて、ベクトルは移動されます。クラスのユーザーは、ほとんどの場合、その右辺値参照を扱いません。


3番目の例がベクターコピーを実行することを本当に確信していますか?
タランチュラ

@タランチュラ:それはあなたのベクトルを破壊します。壊れる前にコピーしたかどうかは問題ではありません。
子犬

4
あなたが提案したつぶしの理由は何も見当たらない。ローカルの右辺値参照変数を右辺値にバインドしても問題ありません。その場合、一時オブジェクトの存続期間は、右辺値参照変数の存続期間まで延長されます。
fredoverflow

1
私はこれを学んでいるので、説明のポイントにすぎません。この新しい例では、ベクターtmpはに移動されませんrval_refrval_refRVO を使用して直接書き込まれます(つまり、コピー省略)。std::moveとコピー省略の間には違いがあります。Aにstd::moveは、コピーされるデータがまだ含まれている場合があります。ベクトルの場合、新しいベクトルは実際にはコピーコンストラクターで構築され、データが割り当てられますが、データ配列の大部分はポインターを(本質的に)コピーすることによってのみコピーされます。コピーの省略により、すべてのコピーが100%回避されます。
Mark Lakata 2014

@MarkLakataこれはRVOではなくNRVOです。C ++ 17でも、NRVOはオプションです。適用されない場合、戻り値とrval_ref変数の両方がのmoveコンストラクタを使用して構築されstd::vectorます。あり/なしの両方に関係するコピーコンストラクタはありませんstd::move。この場合、ステートメントでtmp右辺値として扱われreturnます。
Daniel Langr

16

簡単な答えは、通常の参照コードと同じように右辺値参照のコードを記述し、99%の時間でそれらを同じように扱う必要があるということです。これには、参照を返すことに関するすべての古いルールが含まれます(つまり、ローカル変数への参照を返すことはありません)。

std :: forwardを利用し、左辺値または右辺値参照のいずれかを取る汎用関数を記述できるテンプレートコンテナクラスを作成している場合を除き、これは多かれ少なかれ当てはまります。

moveコンストラクターとmove割り当ての大きな利点の1つは、それらを定義すると、RVO(戻り値の最適化)とNRVO(名前付き戻り値の最適化)が呼び出されなかった場合にコンパイラーがそれらを使用できることです。これは、コンテナや文字列などの高価なオブジェクトを、メソッドから効率的に値で返すには非常に巨大です。

ここで、右辺値参照で興味深い点は、通常の関数への引数としても使用できることです。これにより、const参照(const foo&other)とrvalue参照(foo && other)の両方のオーバーロードを持つコンテナーを作成できます。引数が扱いにくく、単なるコンストラクタ呼び出しでは渡せない場合でも、次のように実行できます。

std::vector vec;
for(int x=0; x<10; ++x)
{
    // automatically uses rvalue reference constructor if available
    // because MyCheapType is an unamed temporary variable
    vec.push_back(MyCheapType(0.f));
}


std::vector vec;
for(int x=0; x<10; ++x)
{
    MyExpensiveType temp(1.0, 3.0);
    temp.initSomeOtherFields(malloc(5000));

    // old way, passed via const reference, expensive copy
    vec.push_back(temp);

    // new way, passed via rvalue reference, cheap move
    // just don't use temp again,  not difficult in a loop like this though . . .
    vec.push_back(std::move(temp));
}

STLコンテナーが更新されて、ほとんどすべて(ハッシュキーと値、ベクトル挿入など)の移動オーバーロードがあり、最も多く表示されます。

これらを通常の関数に使用することもできます。右辺値参照引数のみを指定する場合は、呼び出し元にオブジェクトを作成させ、関数に移動させることができます。これは本当に良い使い方というよりは例ですが、私のレンダリングライブラリでは、読み込まれたすべてのリソースに文字列を割り当てたので、デバッガーで各オブジェクトが何を表しているかを簡単に確認できます。インターフェースは次のようなものです:

TextureHandle CreateTexture(int width, int height, ETextureFormat fmt, string&& friendlyName)
{
    std::unique_ptr<TextureObject> tex = D3DCreateTexture(width, height, fmt);
    tex->friendlyName = std::move(friendlyName);
    return tex;
}

これは「漏れやすい抽象化」の形式ですが、ほとんどの場合、すでに文字列を作成しなければならなかったという事実を利用して、それをさらにコピーすることを避けることができます。これは厳密には高性能のコードではありませんが、この機能のコツをつかむ可能性の良い例です。このコードでは、実際には、変数が呼び出しの一時的なものであるか、またはstd :: moveが呼び出されている必要があります。

// move from temporary
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string("Checkerboard"));

または

// explicit move (not going to use the variable 'str' after the create call)
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, std::move(str));

または

// explicitly make a copy and pass the temporary of the copy down
// since we need to use str again for some reason
string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, string(str));

しかし、これはコンパイルされません!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);

3

それ自体は答えではなく、ガイドラインです。(で行ったように)ほとんどの場合、ローカルT&&変数の宣言はあまり意味がありませんstd::vector<int>&& rval_ref。型メソッドでstd::move()使用するには、それらを使用する必要がありfoo(T&&)ます。あなたがそのようなものを返そうとするとrval_ref、関数ものと、破壊された一時的な大失敗への標準参照が表示ます。

ほとんどの場合、私は次のパターンで行きます:

// Declarations
A a(B&&, C&&);
B b();
C c();

auto ret = a(b(), c());

返された一時オブジェクトへの参照を保持しないため、移動したオブジェクトを使用したい(経験の浅い)プログラマーのエラーを回避できます。

auto bRet = b();
auto cRet = c();
auto aRet = a(std::move(b), std::move(c));

// Either these just fail (assert/exception), or you won't get 
// your expected results due to their clean state.
bRet.foo();
cRet.bar();

(かなりまれではありますが)関数が非一時的なものT&&への参照であるa を本当に返す場合があります。、オブジェクトに移動できるオブジェクトます。

RVOについて:これらのメカニズムは一般に機能し、コンパイラーはコピーをうまく回避できますが、戻りパスが明確でない場合(例外、if条件付きで返される名前付きオブジェクトを決定し、おそらく他のものを結合します)、rrefはあなたの救世主です(潜在的にもっと多くの場合でも)高価な)。


2

それらのどれも余分なコピーを行いません。RVOが使用されていない場合でも、新しい標準では、移動を行う場合はコピーを作成するほうが好ましいと考えています。

ローカル変数への参照を返すため、2番目の例では未定義の動作が発生すると思います。


1

最初の回答へのコメントですでに述べたように、return std::move(...);構造はローカル変数の戻り以外の場合に違いを生む可能性があります。以下は、メンバーオブジェクトを返す場合と持たない場合の戻り値を示した実行可能な例ですstd::move()

#include <iostream>
#include <utility>

struct A {
  A() = default;
  A(const A&) { std::cout << "A copied\n"; }
  A(A&&) { std::cout << "A moved\n"; }
};

class B {
  A a;
 public:
  operator A() const & { std::cout << "B C-value: "; return a; }
  operator A() & { std::cout << "B L-value: "; return a; }
  operator A() && { std::cout << "B R-value: "; return a; }
};

class C {
  A a;
 public:
  operator A() const & { std::cout << "C C-value: "; return std::move(a); }
  operator A() & { std::cout << "C L-value: "; return std::move(a); }
  operator A() && { std::cout << "C R-value: "; return std::move(a); }
};

int main() {
  // Non-constant L-values
  B b;
  C c;
  A{b};    // B L-value: A copied
  A{c};    // C L-value: A moved

  // R-values
  A{B{}};  // B R-value: A copied
  A{C{}};  // C R-value: A moved

  // Constant L-values
  const B bc;
  const C cc;
  A{bc};   // B C-value: A copied
  A{cc};   // C C-value: A copied

  return 0;
}

おそらく、return std::move(some_member);特定のクラスメンバーを実際に移動したい場合にのみ意味があります。たとえば、class Cstruct A。です。

オブジェクトがR値の場合でも、struct A常にからコピーされることに注意してください。これは、コンパイラのインスタンスがもう使用されないことをコンパイラが判断できないためです。では、のインスタンスが定数でない限り、コンパイラはからこの情報を取得します。そのため、が移動されます。class Bclass Bclass Bstruct Aclass Cstd::move()struct Aclass C

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.