web-dev-qa-db-ja.com

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

右辺値参照を理解し、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();
401
Tarantula

最初の例

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_refを非constな方法で使用することはできません。

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番目の例は、最初の例とほぼ同じです。 tmpstd::moveは不要であり、戻り値の最適化を妨げるため、実際にはパフォーマンスの悲観化になる可能性があります。

自分のしていることをコーディングするための最良の方法は次のとおりです。

ベストプラクティス

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と同じように。 tmpは、returnステートメントでは暗黙的に右辺値として扱われます。それは戻り値最適化(コピーなし、移動なし)を介して返されるか、またはコンパイラがRVOを実行できないと判断した場合は 戻りにはvectorのmoveコンストラクタを使用します のいずれかです。 RVOが実行されず、返された型に移動コンストラクタがない場合に限り、コピーコンストラクタが戻りに使用されます。

525
Howard Hinnant

それらのどれもコピーしませんが、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();

今を除いて、ベクトルは動かされます。クラスのserは、大部分の場合、右辺値参照を扱いません。

41
Puppy

簡単な答えは、通常の参照コードと同じように右辺値参照のためのコードを書くべきであり、あなたはそれらを精神的に同じ99%の時間で扱うべきです。これは参照を返すことに関するすべての古い規則を含みます(すなわち、ローカル変数への参照を決して返さない)。

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

移動コンストラクターと移動代入の大きな利点の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);
16
Zoner

答えではないそれ自体、しかしガイドライン。ほとんどの場合、(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&&を返す場合があります。これは一時的でないオブジェクトへの参照であり、オブジェクトに移動できます。

RVOに関して:これらのメカニズムは一般的に機能し、コンパイラはうまくコピーを避けることができますが、戻りパスが明白でない場合(例外、ifname__条件式があなたが返す名前オブジェクトを決定し、そしておそらく他を結合します)もっと高い)。

3
Red XIII

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

私はあなたのローカル変数への参照を返しているのであなたの2番目の例は未定義の動作を引き起こすと信じています。

2
Crazy Eddie

最初の答えに対するコメントですでに述べたように、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のインスタンスを作成することのみを目的とした短期間のアダプタオブジェクトを表す場合。

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

0
Andrej Podzimek