C++11 返回值优化还是移动?

222
我不明白什么时候应该使用std::move,什么时候应该让编译器进行优化... 例如:
using SerialBuffer = vector< unsigned char >;

// let compiler optimize it
SerialBuffer read( size_t size ) const
{
    SerialBuffer buffer( size );
    read( begin( buffer ), end( buffer ) );
    // Return Value Optimization
    return buffer;
}

// explicit move
SerialBuffer read( size_t size ) const
{
    SerialBuffer buffer( size );
    read( begin( buffer ), end( buffer ) );
    return move( buffer );
}

我应该使用哪个?

13
本地变量的函数返回值不需要显式移动,这是隐式移动的。 - Martin Ba
4
编译器可以自由选择:如果可能的话,会使用RVO,如果不行,则仍然可以进行移动操作(如果该类型无法移动,则将进行复制)。 - Martin Ba
据我所知,编译器在第一个版本中无法进行任何移动,因为缓冲区是一个左值,它有一个名称,因此不会调用任何移动构造函数。 - Elvis Dukaj
7
@MartinBa,永远不要说永远 ;) 如果本地变量的类型与返回类型不同,则需要显式移动,例如 std::unique_ptr<base> f() { auto p = std::make_unique<derived>(); p->foo(); return p; },但如果类型相同,则会在可能的情况下进行移动(且该移动可能会被省略)。 - Jonathan Wakely
5
为了完整起见,@JonathanWakely所说的已经在一个缺陷报告中得到解决,而且至少是近期版本的gcc和clang不需要在那里显式移动。 - etarion
显示剩余3条评论
4个回答

154

仅使用第一种方法:

Foo f()
{
  Foo result;
  mangle(result);
  return result;
}

如果可用,这将已经允许使用移动构造函数。事实上,当允许复制省略时,局部变量可以在return语句中与右值引用绑定。

你的第二个版本积极禁止复制省略。第一个版本是普遍更好的。


5
即使禁用了复制省略(-fno-elide-constructors),移动构造函数仍然会被调用。 - Géry Ogam
@Maggyero: -fno-elide-constructors 不是禁用复制省略,而是禁用返回值优化。前者是一条语言规则,你无法“禁用”它;后者是一种利用此规则的优化。实际上,我的整个观点是,即使不使用返回值优化,你仍然可以使用移动语义,这是相同一组语言规则的一部分。 - Kerrek SB
@Maggyero:听起来像是文档中的一个错误,具体来说,似乎文档措辞没有针对C++11进行更新。要报告一个bug吗?@JonathanWakely? - Kerrek SB
在 C++ 17(C++ 11 和 C++ 14 之前),-fno-elide-constructors 编译选项会禁用所有复制省略,包括返回语句 glvalue/prvalue 对象初始化程序 (这些复制省略分别称为 NRVO/RVO)、变量 prvalue 对象初始化程序、throw 表达式 glvalue 对象初始化程序和 catch 子句 glvalue 对象初始化程序。自从 C++ 17 开始,对于返回语句 prvalue 对象初始化程序和变量 prvalue 对象初始化程序,复制省略是强制性的,因此该选项现在仅禁用剩余情况下的复制省略。 - Géry Ogam
是的,文档中确实写着“复制”而不是“复制或移动”,但由于我们总是谈论“复制省略”,即使是省略移动构造(我们不说“移动省略”),我认为这并不会有害。 - Géry Ogam
显示剩余3条评论

137

所有返回值都已经被移动或优化了,因此不需要显式地移动返回值。

编译器允许自动移动返回值(以优化复制),甚至可以优化掉移动操作!

n3337标准草案第12.8节(C++11):

When certain criteria are met, an implementation is allowed to omit the copy/move construction of a class object, even if the copy/move constructor and/or destructor for the object have side effects. In such cases, the implementation treats the source and target of the omitted copy/move operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization.This elision of copy/move operations, called copy elision, is permitted in the following circumstances (which may be combined to eliminate multiple copies):

[...]

Example:

class Thing {
public:
Thing();
   ~Thing();
   Thing(const Thing&);
};

Thing f() {
   Thing t;
   return t;
}

Thing t2 = f();

Here the criteria for elision can be combined to eliminate two calls to the copy constructor of class Thing: the copying of the local automatic object t into the temporary object for the return value of function f() and the copying of that temporary object into object t2. Effectively, the construction of the local object t can be viewed as directly initializing the global object t2, and that object’s destruction will occur at program exit. Adding a move constructor to Thing has the same effect, but it is the move construction from the temporary object to t2 that is elided. — end example ]

When the criteria for elision of a copy operation are met or would be met save for the fact that the source object is a function parameter, and the object to be copied is designated by an lvalue, overload resolution to select the constructor for the copy is first performed as if the object were designated by an rvalue. If overload resolution fails, or if the type of the first parameter of the selected constructor is not an rvalue reference to the object’s type (possibly cv-qualified), overload resolution is performed again, considering the object as an lvalue.


14
我不是特别喜欢整个“编译器可以做X”的论点。这个问题不需要求助于任何编译器,它纯粹与语言有关。关于“移动”是否发生,没有任何“可选”的或模糊的地方。语言非常清楚,哪些构造函数参数可以绑定到返回值(即xvalue);重载决议会完成其余部分。 - Kerrek SB
5
重点不在于编译器能做什么,而是主要编译器实际上做了什么。 明确地移动对象可能会妨碍编译器更好地执行操作。 几乎所有足够先进的可以允许您明确移动对象的编译器都足够先进,以自动移动返回值 - 因为与其他您可能想要明确移动的情况不同,编译器非常容易检测到返回值作为优化的好位置(因为任何返回都保证该值不会在执行返回的函数中继续使用)。 - Jamin Grey
3
@ Damon:嗯,有点像。编译器可以移动返回值(并保存副本),但它们通常不这样做。相反,它们在可能的情况下使用复制省略,这既节省了复制移动。它们直接赋值给接收函数结果的变量,而不是返回并稍后分配的临时变量。手动移动变量从来没有更好,而且通常略微(只是稍微)比编译器所做的更糟。编译器会退回到移动语义,但在可能的情况下更愿意使用RVO。至少,那是我的理解。 - Jamin Grey
4
如果类型不匹配,则所有返回值均已移动或优化掉。参考链接:https://groups.google.com/a/isocpp.org/forum/#!msg/std-proposals/Tc1p52jg-1Y/ntMVTa0F38cJ - cdyson37
1
@cdyson37很有趣,我以前从未遇到过那种边缘情况。幸运的是,如果没有std::move(),它甚至无法编译。我正在尝试弄清楚这个示例是否实际上展示了预期的语言特性的一部分,还是利用了模板成员函数的意外怪癖(在这种情况下,std::unique_ptr()的模板化移动构造函数)。 - Jamin Grey
显示剩余8条评论

93

这很简单。

return buffer;

如果您这样做,那么NRVO要么会发生,要么不会发生。如果没有发生,则buffer将被移动。

return std::move(buffer);

如果您这样做,则NVRO 将不会发生,并且buffer将被移动。

因此,在这里使用std::move没有任何好处,而且可能会损失更多。


上述规则有一个例外*:

Buffer read(Buffer&& buffer) {
    //...
    return std::move( buffer );
}
如果buffer是右值引用,那么您应该使用std::move。这是因为引用不符合NRVO的条件,所以如果没有std::move,它将导致从左值进行复制。
这只是“始终移动右值引用和通用引用”规则的一个实例,该规则优先于“永远不要移动返回值”的规则。
* 自C++20起,可以忘记此异常。现在,return语句中的右值引用会被隐式移动。

11
非常重要的例外,谢谢。我在我的代码中刚刚遇到了这个问题。 - Mikhail
1
编程语言处于一种有趣的状态,必须使用记忆术来编码决策树,以便执行简单的操作而不复制返回值。C++ 中的移动语义和右值普遍被认为是设计成功的体现吗?对我来说,它们显然是一个复杂的解决方案,用于解决看起来很简单的问题。再加上隐式使用 NVRO,这肯定会导致设计非常混乱。 - ldog
@ldog,对于许多设计决策,不仅关注C++,几乎总是在利弊之间取得平衡。如果考虑到rvalue引用的所有优点,通过明确地返回std::move(...)来进行意外手动抑制RVO/NRVO似乎是可以接受的风险,特别是如果错误以非常显式的方式完成。由于自C++11以来,rvalue函数参数是语言中的新功能,所以现有的旧代码或“已建立的样式习惯”不太可能被意外破坏。自C++17以来的保证复制省略进一步有助于记住这些事情。 - Secundi

35

如果你要返回一个本地变量,请不要使用move()。这将允许编译器使用NRVO,如果无法使用NRVO,则编译器仍然可以执行移动操作(在return语句中,本地变量成为R值)。在这种情况下使用move()只会抑制NRVO并强制编译器使用移动(如果移动不可用则进行复制)。如果你要返回的不是本地变量,那么NRVO无论如何都不是一个选项,你应该使用move(),但只有在想要窃取对象时才需要使用。


如果我重用以下示例: http://en.cppreference.com/w/cpp/language/copy_elision 在返回语句上添加std::move(第17行),不会禁用复制省略。标准实际上指出,复制省略将省略“std :: move”和复制构造函数。 - Thomas Legris
@ThomasLegris,我不理解你的评论。如果你在谈论 return v;,那么在这种形式下,NRVO将省略移动(和复制)。在C++14中,不需要执行移动省略,但需要执行复制省略(必须支持仅移动类型)。我相信在更近期的C++标准中,也需要省略移动(以支持不可移动类型)。如果该行改为 return std::move(v);,则您不再返回局部变量;您正在返回一个表达式,因此NRVO不符合条件 --- 需要移动(或复制)。 - Adam H. Peterson
似乎编译器足够聪明,可以删除 std::move 并应用 NRVO。在 第17行 添加 return std::move(v); 经验证显示既不调用移动构造函数也不调用复制构造函数(您可以通过点击“运行”并选择编译器选项“gcc 4.7 C++11”来尝试)。然而,Clang 输出警告但仍能应用 NRVO。因此,我认为最好的做法是不添加 std::move,但添加它不一定会完全禁止 NRVO,这就是我的观点。 - Thomas Legris
2
@ThomasLegris,好的,我明白你所看到的,但我有一个替代解释。实际上正在执行移动操作,但移动的是vector<Noisy>而不是Noisyvector<>的移动构造函数可以通过指针操作移动包含的对象,因此不必移动单个对象。如果将函数更改为直接使用Noisy而不是vector<Noisy>,则移动会显示出来。 - Adam H. Peterson
@ThomasLegris,如果您感兴趣的话,可以用array<Noisy,3>替换vector<Noisy>来查看该示例中的移动操作。这样可以让您将移动与对象容器结合起来看,但是对象直接作为值聚合到数据类型中,而不是隐藏在允许STL优化掩盖移动的freestore分配背后。(这可能是对cppreference.com页面进行的良好更改,以更直接地说明基于值的移动和复制/移动省略。) - Adam H. Peterson

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