【问题标题】:When should std::move be used on a function return value? [duplicate]什么时候应该在函数返回值上使用 std::move? [复制]
【发布时间】:2016-03-09 09:32:37
【问题描述】:

在这种情况下

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

我想需要采取行动吗?

【问题讨论】:

  • 使用 Visual Studio 时。
  • 在第二种情况下仅供参考,当您从函数返回时,您不能有任何可用的 foo 引用/指针。
  • 你对返回的值做了什么? Foo f = meh(); 已在 C++98 中使用 (N)RVO。
  • 我想知道显式调用 std::move 是否会阻止 NRVO...
  • std::move 是一个身份操作。它实际上从不做任何事情。它只是右值的标记。如果编译器手头有 Foo 的移动构造函数,它可以查看它是否具有可观察的效果并做出决定。如果它没有可观察到的影响,你怎么能分辨出来?

标签: c++ c++11 move-semantics


【解决方案1】:

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

当满足或将满足省略复制操作的条件时 遇到源对象是函数参数这一事实, 并且要复制的对象由左值指定,重载 为副本选择构造函数的决议首先执行为 如果对象是由右值指定的。

return foo; 是 NRVO 的一种情况,因此允许复制省略。 foo 是一个左值。因此,从foo“复制”到meh的返回值所选择的构造函数必须是移动构造函数(如果存在)。

添加 move 确实有一个潜在的影响:它可以防止移动被忽略,因为 return std::move(foo); 符合 NRVO 的条件。

据我所知,12.8/32 列出了 only 条件,在这些条件下,左值的副本可以被移动替换。通常不允许编译器在复制后检测到未使用的左值(例如使用 DFA),并主动进行更改。我在这里假设两者之间存在可观察到的差异——如果可观察到的行为相同,则适用“as-if”规则。

因此,要回答标题中的问题,当您希望它被移动并且它不会被移动时,请在返回值上使用std::move。那就是:

  • 您希望它被移动,并且
  • 它是一个左值,并且
  • 它不符合复制省略的条件,并且
  • 它不是按值函数参数的名称。

考虑到这非常繁琐,而且移动通常很便宜,你可能想说在非模板代码中你可以稍微简化一下。在以下情况下使用std::move

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

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

【讨论】:

  • C++:简单。很明显。
  • C++:不笑就会哭。
  • 当函数声明为返回std::unique_ptr<Base> 时,尝试返回声明为std::unique_ptr<Derived> 的变量会怎样?在 gcc 和 mingw-w64 中它可以正常工作,但 vanilla mingw(基于 gcc 4.9.3,针对 i686-pc-cygwin)需要 std::move(x) 编译。
  • @rr:不确定。我希望在这种情况下需要std::move(因为类型不匹配,所以复制省略不在桌面上),但我可能会忽略一些东西。 32 位和 64 位 mingw 之间存在差异的事实很奇怪。我想不出编译器或平台作者会这样做的原因。
  • 谢谢,这是有道理的。简单地说,mingw-w64 不仅仅是 mingw 的 64 位版本——它是一个引入了一些实质性更新的分支。
【解决方案2】:

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

【讨论】:

  • 使用std::move被认为是有害的,可以防止省略
  • 更正:使用 std::move 返回值被认为是有害的,可以防止省略
【解决方案3】:

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

【讨论】:

  • 需要注意的是,如果三元组的其中一个参数是原地创建的,则直接构造成返回变量。然而,采取行动将防止这种情况发生。这使得第二种选择更加灵活 - 只会移动命名的 arg。
【解决方案4】:

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

这是一个人为的例子,说明何时应该使用它:

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

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

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

【讨论】:

  • int 上使用std::move 没有任何好处;如果x 是具有昂贵副本的类类型(例如附加到字符串),这可能是一个更好的例子
  • @M.M 接受改进
【解决方案5】:

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

【讨论】:

  • @M.M 在这种情况下,我不太关心 C++ 术语。答案中的表达式“调用 std::move”的含义等同于“使用 std::move”。
  • @M.M 您对如何改进答案有进一步的建议吗?
  • @atomsymbol 这是一个很好的信息性答案,增加了现有答案,我不知道大惊小怪。
【解决方案6】:

std::move 从函数返回时完全没有必要,它真正进入了你的领域——程序员——试图照看你应该留给编译器的东西。

当您 std::move 某个函数的某些东西不是该函数的局部变量时会发生什么?你可以说你永远不会写那样的代码,但是如果你写的代码很好,然后重构它并且心不在焉地不改变std::move,会发生什么。您会很高兴跟踪该错误。

另一方面,编译器几乎不会犯这类错误。

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

See here.

【讨论】:

  • 最后一段的注释是错误的。编译器需要将其视为右值。
  • @R.MartinhoFernandes 是的,不是的,它将被视为右值,但我的编译器更喜欢省略移动构造,因此可以说没有移动语义(因为move 构造函数根本不会被调用)
  • 第一段错误。返回std::move 是一个坏主意,这几乎总是正确的,但在某些情况下,返回std::move 是正确的做法。
猜你喜欢
  • 2015-06-04
  • 2011-03-25
  • 2019-05-01
  • 2017-07-24
  • 2012-09-15
相关资源
最近更新 更多