489

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

6 回答 6

626

第一个例子

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::moveon是不必要的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 语句中被隐式视为右值。它将通过返回值优化(不复制,不移动)返回,或者如果编译器决定它不能执行 RVO,那么它将使用向量的移动构造函数来执行 return。仅当不执行 RVO 并且返回的类型没有移动构造函数时,才会使用复制构造函数进行返回。

于 2011-02-13T20:52:04.650 回答
45

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

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

于 2011-02-13T20:31:35.860 回答
16

简单的答案是您应该像编写常规引用代码一样为右值引用编写代码,并且您应该在 99% 的时间里以同样的方式对待它们。这包括所有关于返回引用的旧规则(即永远不要返回对局部变量的引用)。

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

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

现在,右值引用的有趣之处在于,您还可以将它们用作普通函数的参数。这允许您编写具有 const 引用(const foo& other)和右值引用(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);
于 2011-07-10T00:56:07.247 回答
4

本身不是答案,而是指导方针。大多数情况下,声明局部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();

显然,在某些情况下(尽管相当罕见),函数真正返回 a是对可以移动到对象中的非临时T&&对象的引用。

关于 RVO:这些机制通常有效,编译器可以很好地避免复制,但在返回路径不明显的情况下(异常、if条件确定您将返回的命名对象,并且可能结合其他)rrefs 是您的救星(即使可能更多昂贵的)。

于 2013-02-26T07:46:40.867 回答
3

这些都不会做任何额外的复制。即使不使用 RVO,新标准也表示我相信在进行退货时首选移动构造而不是复制。

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

于 2011-02-13T20:40:33.517 回答
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 C表示短期适配器对象的情况下,其唯一目的是创建struct A.

请注意,即使对象是 R 值,struct A总是如何从 中复制出来。这是因为编译器无法告诉' 的实例将不再被使用。在中,编译器确实有来自 的信息,这就是为什么会被移动,除非 的实例是常量。class Bclass Bclass Bstruct Aclass Cstd::move()struct Aclass C

于 2019-03-06T09:52:41.777 回答