什么时候应该对函数返回值使用 std: : move?

在这种情况下

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

我非常肯定这个移动是不必要的,因为新创建的 Foo将是一个 x 值。

但是像这样的案子呢?

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

我想,这就是需要搬家的地方吧?

92933 次浏览

在这两种情况下,此举都没有必要。在第二种情况下,std::move是多余的,因为您是通过值返回一个局部变量,编译器将理解,因为您不再使用该局部变量,所以可以将它从而不是被复制。

当从函数返回时,std::move是完全不必要的,并且真正进入了您(程序员)的领域——试图照看您应该留给编译器的事情。

如果 std::move函数中的某个变量不是该函数的本地变量,会发生什么情况?您可以说您永远不会编写这样的代码,但是如果您编写的代码没有问题,然后重构它,并且漫不经心地不改变 std::move,那么会发生什么呢。追踪那个窃听器会很有意思的。

另一方面,编译器基本上不会犯这种错误。

另外: 需要注意的是,从函数返回局部变量时,没有必须创建一个 rvalue 或使用 move 语义。

看这里。

return std::move(foo);而言,move是多余的,因为12.8/32:

当满足省略复制操作的条件时 除非源对象是一个函数参数, 要复制的对象由一个左值指定,重载 选择复制的构造函数的解析首先执行为 如果对象是由右值指定的。

return foo;是 NRVO 的一种情况,因此允许复制省略。foo是左值。因此,为从 foomeh的返回值的“拷贝”选择的构造函数必须是 move 构造函数(如果存在的话)。

添加 move确实有一个潜在的影响,但是: 它防止移动被省略,因为 return std::move(foo);没有符合 NRVO 的资格。

据我所知,12.8/32给出了一个 只有条件,在这个条件下,左值的副本可以通过移动来替换。通常不允许编译器检测复制后左值是否未使用(比如使用 DFA) ,并主动进行更改。我在这里假设这两者之间有一个可观察到的差异——如果可观察到的行为是相同的,那么“似是而非”规则适用。

因此,为了回答标题中的问题,当您希望返回值被移动而它无论如何也不会被移动时,可以对返回值使用 std::move。那就是:

  • 你希望它被移动
  • 它是一个左值,而且
  • 不符合复本省略的资格,以及
  • 它不是 by-value 函数参数的名称。

考虑到这是相当繁琐和移动是廉价的 通常,您可能想说,在非模板代码,您可以简化这一点。在下列情况下使用 std::move:

  • 你希望它被移动
  • 它是一个左值,而且
  • 你根本不用担心。

通过遵循简化的规则,您牺牲了一些移动省略。对于像 std::vector这样移动成本低廉的类型,你可能永远不会注意到(如果你注意到,你可以优化)。对于像 std::array这样移动成本高昂的类型,或者对于不知道移动成本是否低廉的模板,您更可能会为此而烦恼。

关于什么时候不应该移动它有很多答案,但问题是“什么时候应该移动它?”

下面是一个关于何时应该使用该词的人为例子:

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

也就是说,如果有一个函数接受一个 rvalue 引用,修改它,然后返回它的一个副本。(在 的行为在这里改变)现在,在实践中,这种设计几乎总是更好:

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

它还允许您使用非 rvalue 参数。

基本上,如果在要移动返回的函数中有一个右值引用,就必须调用 std::move。如果您有一个局部变量(不管它是否是一个参数) ,隐式返回它 move(这个隐式移动可以省略掉,而显式移动不能省略)。如果有一个函数或操作接受本地变量,并返回对所述本地变量的引用,则必须使用 std::move才能进行 move 操作(例如,三进制 ?:操作符)。

在返回值上,如果返回表达式直接引用本地 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可能会影响其他优化。

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

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

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


概念上 说,year-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的复制构造函数和析构函数。


Year-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年的程序员需要明确地指定这样的优化。