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



在这种情况下,

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

我想需要搬家吗?

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。即:

  • 你希望它被移动,并且
  • 它是一个左值,并且
  • 它没有资格删除副本,并且
  • 它不是按值函数参数的名称

考虑到这很麻烦,而且通常很便宜,您可能会说,在非模板代码中,您可以稍微简化一下。当:时使用std::move

  • 你希望它被移动,并且
  • 它是一个左值,并且
  • 你不必担心它

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

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

在返回值上,如果返回表达式直接引用本地左值的名称(即此时的x值),则不需要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可能会影响其他优化。

关于何时不应该移动有很多答案,但问题是"什么时候应该移动"

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

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

也就是说,当你有一个函数接受一个右值引用,修改它,然后返回它的副本。(在c++20中,这里的行为发生了变化)现在,在实践中,这种设计几乎总是更好的:

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

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

基本上,如果在函数中有一个要通过移动返回的右值引用,则必须调用std::move。如果您有一个局部变量(无论它是否是参数),则隐式返回它moves(这种隐式移动可以被消除,而显式移动不能)。如果您有一个函数或操作,它接受局部变量,并返回对所述局部变量的引用,则必须使用std::move才能进行移动(例如,三元?:运算符)。

C++编译器可以免费使用std::move(foo):

  • 如果已知foo处于其寿命的末尾,并且
  • 除了C++规范所允许的语义效果之外,CCD_ 25的隐式使用不会对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();
}

避免了调用CCD_ 28的复制构造函数和析构函数。


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

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

当你从一个函数中std::move某个不是该函数局部变量的东西时,会发生什么?你可以说你永远不会写这样的代码,但如果你写的代码很好,然后重构它,并且心不在焉地不更改std::move,会发生什么呢。追踪那个bug你会很开心的。

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

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

请参见此处。

最新更新