C++11 右值和带有 return 语句的移动语义

发布于 10-17 08:39 字数 718 浏览 9 评论 0原文

我试图理解 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();

I'm trying to understand rvalue references and move semantics of C++11.

What is the difference between these examples, and which of them is going to do no vector copy?

First example:

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

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

Second example:

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

Third example:

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(6

℡Ms空城旧梦2024-10-24 08:39:01

这些都不会进行任何额外的复制。即使不使用 RVO,新标准也表明在进行回报时,移动构建优先于复制,我相信。

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

None of those will do any extra copying. Even if RVO isn't used, the new standard says that move construction is preferred to copy when doing returns I believe.

I do believe that your second example causes undefined behavior though because you're returning a reference to a local variable.

独木成林2024-10-24 08:39:01

正如第一个答案的评论中已经提到的,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 实例的情况下,这是有意义的。

请注意,即使 class B 对象是 R-,struct A 总是如何从 class B复制价值。这是因为编译器无法告知 class Bstruct A 实例将不再被使用。在 class C 中,编译器确实从 std::move() 获取了此信息,这就是 struct A 被移动的原因/em>,除非class C 的实例是常量。

As already mentioned in comments to the first answer, the return std::move(...); construct can make a difference in cases other than returning of local variables. Here's a runnable example that documents what happens when you return a member object with and without 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;
}

Presumably, return std::move(some_member); only makes sense if you actually want to move the particular class member, e.g. in a case where class C represents short-lived adapter objects with the sole purpose of creating instances of struct A.

Notice how struct A always gets copied out of class B, even when the class B object is an R-value. This is because the compiler has no way to tell that class B's instance of struct A won't be used any more. In class C, the compiler does have this information from std::move(), which is why struct A gets moved, unless the instance of class C is constant.

木有鱼丸2024-10-24 08:39:00

第一个示例

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

您的第三个示例大致相当于您的第一个示例。 tmp 上的 std::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,则它 将使用向量的移动构造函数来执行返回。仅当不执行 RVO,并且返回的类型没有移动构造函数时,才会使用复制构造函数进行返回。

First example

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

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

The first example returns a temporary which is caught by rval_ref. That temporary will have its life extended beyond the rval_ref definition and you can use it as if you had caught it by value. This is very similar to the following:

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

except that in my rewrite you obviously can't use rval_ref in a non-const manner.

Second example

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

In the second example you have created a run time error. rval_ref now holds a reference to the destructed tmp inside the function. With any luck, this code would immediately crash.

Third example

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

Your third example is roughly equivalent to your first. The std::move on tmp is unnecessary and can actually be a performance pessimization as it will inhibit return value optimization.

The best way to code what you're doing is:

Best practice

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

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

I.e. just as you would in C++03. tmp is implicitly treated as an rvalue in the return statement. It will either be returned via return-value-optimization (no copy, no move), or if the compiler decides it can not perform RVO, then it will use vector's move constructor to do the return. Only if RVO is not performed, and if the returned type did not have a move constructor would the copy constructor be used for the return.

在风中等你2024-10-24 08:39:00

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

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

None of them will copy, but the second will refer to a destroyed vector. Named rvalue references almost never exist in regular code. You write it just how you would have written a copy in 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();

Except now, the vector is moved. The user of a class doesn't deal with it's rvalue references in the vast majority of cases.

抱着落日2024-10-24 08:39:00

简单的答案是,您应该像编写常规引用代码一样为右值引用编写代码,并且在 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 invoked:

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

The simple answer is you should write code for rvalue references like you would regular references code, and you should treat them the same mentally 99% of the time. This includes all the old rules about returning references (i.e. never return a reference to a local variable).

Unless you are writing a template container class that needs to take advantage of std::forward and be able to write a generic function that takes either lvalue or rvalue references, this is more or less true.

One of the big advantages to the move constructor and move assignment is that if you define them, the compiler can use them in cases were the RVO (return value optimization) and NRVO (named return value optimization) fail to be invoked. This is pretty huge for returning expensive objects like containers & strings by value efficiently from methods.

Now where things get interesting with rvalue references, is that you can also use them as arguments to normal functions. This allows you to write containers that have overloads for both const reference (const foo& other) and rvalue reference (foo&& other). Even if the argument is too unwieldy to pass with a mere constructor call it can still be done:

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

The STL containers have been updated to have move overloads for nearly anything (hash key and values, vector insertion, etc), and is where you will see them the most.

You can also use them to normal functions, and if you only provide an rvalue reference argument you can force the caller to create the object and let the function do the move. This is more of an example than a really good use, but in my rendering library, I have assigned a string to all the loaded resources, so that it is easier to see what each object represents in the debugger. The interface is something like this:

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;
}

It is a form of a 'leaky abstraction' but allows me to take advantage of the fact I had to create the string already most of the time, and avoid making yet another copying of it. This isn't exactly high-performance code but is a good example of the possibilities as people get the hang of this feature. This code actually requires that the variable either be a temporary to the call, or std::move invoked:

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

or

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

or

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

but this won't compile!

string str("Checkerboard");
TextureHandle htex = CreateTexture(128, 128, A8R8G8B8, str);
眼眸里的那抹悲凉2024-10-24 08:39:00

本身不是答案,而是指南。大多数时候,声明本地 T&& 变量没有多大意义(就像您对 std::vector&& 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 条件确定您将返回的命名对象,可能还有其他)是你的救星(即使可能更昂贵)。

Not an answer per se, but a guideline. Most of the time there is not much sense in declaring local T&& variable (as you did with std::vector<int>&& rval_ref). You will still have to std::move() them to use in foo(T&&) type methods. There is also the problem that was already mentioned that when you try to return such rval_ref from function you will get the standard reference-to-destroyed-temporary-fiasco.

Most of the time I would go with following pattern:

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

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

You don't hold any refs to returned temporary objects, thus you avoid (inexperienced) programmer's error who wish to use a moved object.

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

Obviously there are (although rather rare) cases where a function truly returns a T&& which is a reference to a non-temporary object that you can move into your object.

Regarding RVO: these mechanisms generally work and compiler can nicely avoid copying, but in cases where the return path is not obvious (exceptions, if conditionals determining the named object you will return, and probably couple others) rrefs are your saviors (even if potentially more expensive).

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文