何时应该在函数返回值上使用std::move?

169

在这种情况下

struct Foo {};
Foo meh() {
  return std::move(Foo());
}

我相信此操作是不必要的,因为新创建的 Foo 将是一个 xvalue。

但是在这种情况下呢?

struct Foo {};
Foo meh() {
  Foo foo;
  //do something, but knowing that foo can safely be disposed of
  //but does the compiler necessarily know it?
  //we may have references/pointers to foo. how could the compiler know?
  return std::move(foo); //so here the move is needed, right?
}

我想那儿需要搬家,是吗?


22
当你使用 Visual Studio。 - R. Martinho Fernandes
4
在第二种情况下,需要说明的是当函数返回时,你将无法再使用任何指向foo的可用引用或指针。 - R. Martinho Fernandes
2
你对返回值做了什么?在C++98中,Foo f = meh();已经使用了(N)RVO。 - Bo Persson
1
std::move是一个标识符操作。它实际上并不执行任何操作,只是rvalue的标记。如果编译器手头有Foo的移动构造函数,它可以看看是否有可观察的影响,并根据这个决定。如果没有可观察的影响,你怎么能区分呢? - R. Martinho Fernandes
1
如果我们对foo有引用/指针,当return语句出现后,无论是否使用了std::move,都不允许继续使用它们。 - user253751
显示剩余6条评论
6个回答

171
return std::move(foo);的情况下,由于12.8/32的规定,move是多余的:
当满足复制操作省略的条件或仅函数参数不满足条件时,且将要复制的对象通过左值表示时,选择复制构造函数的重载解析首先按照对象被表示为右值时的方式进行。 return foo;是NRVO的一种情况,因此允许复制省略。foo是一个左值,所以从foo到meh返回值的“复制”所选的构造函数如果存在,则必须为移动构造函数。
但是,添加move确实有潜在的影响:它会防止移动被省略,因为return std::move(foo);不符合NRVO的条件。
据我所知,12.8/32规定了仅在 lvalue 复制可以替换为移动的唯一条件。通常情况下编译器不允许检测 lvalue 是否在复制后未使用(例如,使用 DFA),并自行采取更改。在这里,我假设两者之间存在可观察的差异——如果可观察行为相同则适用“as-if”规则。
因此,回答标题中的问题,只有当需要移动返回值且它不会被移动时才使用std::move。也就是说:
- 您希望它被移动,而且 - 它是一个左值,并且 - 它不符合复制省略的条件,并且 - 它不是按值传递的函数参数的名称。

考虑到这相当繁琐,而且移动通常很便宜,您可能想说,在非模板代码中可以稍微简化此过程。 当:

  • 您希望对其进行移动,并且
  • 它是左值,并且
  • 您不想担心它。

通过遵循简化规则,您会牺牲一些移动省略。 对于像std::vector这样的类型,移动操作很便宜,您可能永远不会注意到(如果确实注意到,则可以进行优化)。 对于像std::array这样昂贵的移动操作,或者对于您不知道移动操作是否便宜的模板,您更有可能担心。


77
C++:非常简单,易于理解。 - Lightness Races in Orbit
79
C++:如果你不笑,你就会哭。 - mackenir
2
当函数声明返回std::unique_ptr<Base>时,尝试返回声明为std::unique_ptr<Derived>的变量怎么样?在gcc和mingw-w64中,它可以正常工作,但是基于gcc 4.9.3,针对i686-pc-cygwin的vanilla mingw需要使用std::move(x)才能编译。 - rr-
@rr: 不确定。我希望在这种情况下需要使用std::move(因为类型不匹配,因此复制省略不可行),但我可能忽略了某些东西。它在32位和64位mingw之间的差异很奇怪。我无法想到编译器或平台作者会有意这样做的原因。 - Steve Jessop
谢谢,那很有道理。只是为了澄清一下,mingw-w64不仅仅是mingw的64位版本 - 它是一个分支,引入了一些重大更新。 - rr-
如果想保留移动省略,但禁止复制构造函数而不删除(或使其为私有),该怎么办? - Kentzo

51

在这两种情况下,使用std::move是不必要的。在第二种情况下,std::move多余,因为您正在通过值返回本地变量,编译器会理解,由于您不再使用该本地变量,可以将其移动而不是复制。


7
使用std::move被认为是有害的,可能会阻止省略。 - Seth Carnegie
35
更正:使用 std::move 用于返回值被认为是有害的,可能会阻止省略。 - Andreas Magnusson

34

对于返回值,如果返回表达式直接引用了一个本地 lvalue 的名称(也就是此时为 xvalue),则不需要使用 std::move。另一方面,如果返回表达式不是标识符,则不会自动移动,因此例如在这种情况下,您需要显式使用 std::move

T foo(bool which) {
   T a = ..., b = ...;
   return std::move(which? a : b);
   // alternatively: return which? std::move(a), std::move(b);
}

当直接返回一个命名的本地变量或临时表达式时,应避免显式使用std::move。编译器在这些情况下必须(并将来会)自动移动,添加std::move可能会影响其他优化。


1
需要注意的是,如果三元运算符的参数之一是在原地创建的,它将直接构造到返回变量中。但是,对其进行移动将防止这种情况发生。这使第二种选择更加灵活——只会移动命名参数。 - user362515

32

有很多关于不应该移动的答案,但问题是“何时应该移动?”

这里有一个人为制造的例子,说明何时应该使用它:

std::vector<int> append(std::vector<int>&& v, int x) {
  v.push_back(x);
  return std::move(v);
}

例如,当您有一个函数接受一个右值引用,对其进行修改,然后返回其副本时。(在中此处的行为会发生变化) 现在,在实践中,这个设计几乎总是更好的:

std::vector<int> append(std::vector<int> v, int x) {
  v.push_back(x);
  return v;
}

这也允许您使用非右值参数。

基本上,如果您在函数中有一个rvalue引用,想通过移动来返回它,您必须调用std::move。如果您有一个局部变量(无论是参数还是其他),返回它会隐式地move(而这个隐式的move可以被省略,而显式的move则不行)。如果您有一个函数或操作需要局部变量,并返回对该局部变量的引用,则必须使用std::move才能进行移动(例如,三元?:运算符)。


7
int类型上使用std::move没有任何好处;如果x是一个有昂贵拷贝开销的类类型(例如向字符串中追加内容),这可能会是更好的例子。 - M.M
1
@M.M 改进已接受 - Yakk - Adam Nevraumont
std::vector<int> append(std::vector<int>&& v, int x) { v.push_back(x); return std::move(v); }在这种情况下,v可以是左值引用,但会将v所引用的内容置于“未定义状态”中吗? - hombre
@hombre std::move只是一个rvalue转换。v显然是一个rvalue引用,而不是lvalue引用;这里不可能进行完美转发。在使用点上,v也是一个lvalue,因此需要将其转换为rvalue引用,以确保该函数正确链接。我怀疑你有一个常见的误解,认为std::move会移动东西。 - Yakk - Adam Nevraumont
我进行了仔细的检查。只有 rvalue 才能作为 v 传递给 append(std::vector<int>&& v ...)。调用者必须使用 std::move 调用此 api。难道 RVO 不能优化返回时的 v 的复制吗? - hombre
通过 std::unique_ptr 的使用,找到了 lvalue 部分的解决方案。 - hombre

3

一个C++编译器可以自由使用std::move(foo):

  • 如果已知foo处于其生命周期的末尾,并且
  • 隐式使用std::move对C++代码的语义除了C++规范允许的语义效果之外没有任何影响。

这取决于C++编译器的优化能力,它是否能够计算从f(foo); foo.~Foo();f(std::move(foo)); foo.~Foo();的哪些转换在性能或内存消耗方面是有利的,同时遵守C++规范规则。


从概念上讲,2017年的C++编译器,例如GCC 6.3.0,能够优化这段代码:

Foo meh() {
    Foo foo(args);
    foo.method(xyz);
    bar();
    return foo;
}

转换为这段代码:

void meh(Foo *retval) {
   new (retval) Foo(arg);
   retval->method(xyz);
   bar();
}

这样可以避免调用Foo的复制构造函数和析构函数。


2017年的C++编译器(例如GCC 6.3.0)无法优化以下代码:

Foo meh_value() {
    Foo foo(args);
    Foo retval(foo);
    return retval;
}

Foo meh_pointer() {
    Foo *foo = get_foo();
    Foo retval(*foo);
    delete foo;
    return retval;
}

将这些代码转化为:
Foo meh_value() {
    Foo foo(args);
    Foo retval(std::move(foo));
    return retval;
}

Foo meh_pointer() {
    Foo *foo = get_foo();
    Foo retval(std::move(*foo));
    delete foo;
    return retval;
}

这意味着,2017年的程序员需要明确指定这些优化。

4
@M.M 在这种情况下,我并不太关心C++术语。答案中的“调用std::move”表达式具有与“使用std::move”的含义等效的意思。 - atomsymbol
@M.M,你对如何改进答案有进一步的建议吗? - atomsymbol
@atomsymbol 这是一个非常好的信息性答案,补充了现有的答案,我不知道为什么会有争议。 - Avin Kavish

-9

std::move 在函数返回时是完全不必要的,而且真正进入了你——程序员——试图照看应该留给编译器处理的事情的领域。

当你从一个不是函数局部变量的函数中 std::move 某些东西时会发生什么?你可以说你永远不会写那样的代码,但如果你编写的代码很好,然后重构它并且不注意地没有更改 std::move,你将会有趣地追踪这个错误。

另外:重要的是要注意,从函数返回局部变量不一定会创建 rvalue 或使用移动语义。

在这里查看。


8
最后一段中的注释是错误的。编译器需要将其视为rvalue。 - R. Martinho Fernandes
3
是和不是,它将被视为rvalue,但我的编译器更喜欢省略(move elision)而非移动构造(move construction),因此可以说基本上不会出现移动语义(因为移动构造函数不会被调用)。 - Benj
5
第一段是错误的。通常情况下,返回 std::move 是一个不好的主意,但存在一些情况下返回 std::move 是正确的做法。 - Yakk - Adam Nevraumont

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接