C ++ 11 rvalues和移动语义混淆(return语句)

问题描述 投票:396回答:6

我试图理解rvalue引用并移动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();

第二个例子

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();

第三个例子

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();
c++ c++11 move-semantics rvalue-reference c++-faq
6个回答
536
投票

第一个例子

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

第二个例子

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();

在第二个示例中,您创建了一个运行时错误。 rval_ref现在提到了函数内部被破坏的tmp。运气好的话,这段代码会立即崩溃。

第三个例子

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();

你的第三个例子大致相当于你的第一个例子。 std::move上的tmp是不必要的,实际上可能是性能上的悲观,因为它会抑制返回值优化。

编码您正在做的事情的最佳方式是:

最佳实践

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语句中被隐含地视为rvalue。它将通过返回值优化(无复制,无移动)返回,或者如果编译器决定它不能执行RVO,那么它将will use vector's move constructor to do the return。仅当未执行RVO时,并且如果返回的类型没有移动构造函数,则复制构造函数将用于返回。


41
投票

它们都不会复制,但第二个会引用一个被破坏的矢量。命名的右值引用几乎从不存在于常规代码中。你只需要在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();

除了现在,矢量被移动。在绝大多数情况下,类的用户不处理它的右值引用。


16
投票

简单的答案是你应该为rvalue引用编写代码,就像你经常引用代码一样,你应该在99%的时间内对它们进行相同的处理。这包括有关返回引用的所有旧规则(即永远不会返回对局部变量的引用)。

除非您正在编写需要利用std :: forward并且能够编写采用左值或右值引用的泛型函数的模板容器类,否则这或多或少都是正确的。

移动构造函数和移动赋值的一个重要优点是,如果定义它们,编译器可以在RVO(返回值优化)和NRVO(命名返回值优化)无法调用的情况下使用它们。这对于从方法中有效地返回昂贵的对象(如容器和字符串)非常重要。

现在,rvalue引用让事情变得有趣,你也可以将它们用作普通函数的参数。这允许您编写具有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&&的情况,它是一个非临时对象的引用,你可以移动到你的对象中。

关于RVO:这些机制通常起作用,编译器可以很好地避免复制,但是在返回路径不明显的情况下(例外,if条件确定您将返回的命名对象,并且可能与其他人一起)rrefs是您的救星(即使可能更贵)。


2
投票

这些都不会做任何额外的复制。即使没有使用RVO,新标准也表示移动构造更适合复制,我相信。

我相信你的第二个例子会导致未定义的行为,因为你正在返回对局部变量的引用。


0
投票

正如在第一个答案的评论中已经提到的那样,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 C表示短命的适配器对象的情况下,唯一的目的是创建struct A的实例。

注意struct A总是被复制出class B,即使class B对象是R值。这是因为编译器无法判断class Bstruct A实例将不再被使用。在class C中,编译器确实从std::move()获得了这些信息,这就是struct A被移动的原因,除非class C的实例是常数。

© www.soinside.com 2019 - 2024. All rights reserved.