这个问题在这里已经有了答案:C++11 rvalues and move semantics chaos (return statement) (6 answers) Closed 4 years ago。
我不明白什么时候应该使用 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 );
}
我应该使用哪个?
move
:现代编译器足够聪明,几乎可以在任何地方使用 RVO,而且它比 move
更有效。但这只是“传闻”,请注意,所以我对记录在案的解释非常感兴趣。
std::unique_ptr<base> f() { auto p = std::make_unique<derived>(); p->foo(); return p; }
,但如果类型相同,它会尽可能移动(并且移动可能是省略)
只使用第一种方法:
Foo f()
{
Foo result;
mangle(result);
return result;
}
这将已经允许使用移动构造函数(如果可用)。事实上,当允许复制省略时,局部变量可以精确地绑定到 return
语句中的右值引用。
您的第二个版本积极禁止复制省略。第一个版本普遍更好。
所有返回值要么已经是 moved
,要么已优化,因此无需显式移动返回值。
允许编译器自动移动返回值(优化副本),甚至优化移动!
n3337 标准草案 (C++11) 的第 12.8 节:
当满足某些条件时,允许实现省略类对象的复制/移动构造,即使对象的复制/移动构造函数和/或析构函数具有副作用。在这种情况下,实现将省略的复制/移动操作的源和目标简单地视为引用同一对象的两种不同方式,并且该对象的销毁发生在两个对象本应被删除的较晚时间。在没有优化的情况下销毁。这种复制/移动操作的省略,称为复制省略,在以下情况下是允许的(可以结合起来消除多个副本): [...] 示例:class Thing { public: Thing(); 〜东西();事物(常量事物&); };事物 f() { 事物 t;返回 t; } 事物 t2 = f();这里可以结合省略的标准来消除对 Thing 类的复制构造函数的两次调用:将本地自动对象 t 复制到函数 f() 的返回值的临时对象中,以及将该临时对象复制到 object t2。实际上,局部对象 t 的构造可以看作是直接初始化全局对象 t2,并且该对象的销毁将在程序退出时发生。向 Thing 添加移动构造函数具有相同的效果,但省略了从临时对象到 t2 的移动构造。 — end example ] 当满足或将满足复制操作的省略条件时,除了源对象是函数参数,并且要复制的对象由左值指定,重载决议选择构造函数因为首先执行复制,就好像对象由右值指定一样。如果重载决议失败,或者如果所选构造函数的第一个参数的类型不是对对象类型的右值引用(可能是 cv 限定的),则再次执行重载决议,将对象视为左值。
这很简单。
return buffer;
如果您这样做,那么 NRVO 将发生或不会发生。如果没有发生,则 buffer
将从中移出。
return std::move( buffer );
如果您这样做,则 NVRO 不会发生,并且 buffer
将被移出。
因此,在这里使用 std::move
没有任何好处,而且会失去很多。
上述规则有一个例外*:
缓冲区读取(Buffer&& buffer) { //... return std::move( buffer );如果 buffer 是一个右值引用,那么你应该使用 std::move。这是因为引用不符合 NRVO 的条件,因此如果没有 std::move,它将导致来自左值的副本。这只是“始终移动右值引用并转发通用引用”规则的一个实例,它优先于“从不移动返回值”规则。
* 从 C++20 开始,可以忘记此异常。现在,return
语句中的右值引用已隐式移出。
如果您要返回一个局部变量,请不要使用 move()
。这将允许编译器使用 NRVO,否则,编译器仍将被允许执行移动(局部变量在 return
语句中变为 R 值)。在该上下文中使用 move()
只会禁止 NRVO 并强制编译器使用移动(如果移动不可用,则使用副本)。如果您返回的不是局部变量,NRVO 无论如何都不是一个选项,如果(且仅当)您打算窃取该对象时,您应该使用 move()
。
return v;
,在这种形式中,NRVO 将省略移动(和副本)。在 C++14 下,不需要执行移动省略,但需要执行复制省略(支持仅移动类型所必需的)。我相信在最近的 C++ 标准中,也需要省略这一举动(以支持固定类型)。如果该行改为 return std::move(v);
,则不再返回局部变量;您正在返回一个表达式,并且 NRVO 不符合条件 --- 需要移动(或复制)。
std::move
并应用 NRVO。在 第 17 行 添加 return std::move(v);
经验表明,移动构造函数和复制构造函数都不会被调用(您可以通过单击“运行它”并选择编译器选项“gcc 4.7 C++”来尝试11")。然而,Clang 会输出警告,但仍然能够应用 NRVO。所以我想不添加 std::move
是非常好的做法,但添加它不一定会完全抑制 NRVO,这就是我的观点。
vector<Noisy>
而不是 Noisy
。 vector<>
的移动构造函数可以通过指针操作移动包含的对象,因此不必移动单个对象。如果您将函数更改为直接使用 Noisy
而不是 vector<Noisy>
,则会显示移动。
vector<Noisy>
替换为 array<Noisy,3>
。这使您可以结合对象容器查看移动,但对象直接作为值聚合到数据类型中,而不是隐藏在允许 STL 优化掩盖移动的 freestore 分配后面。 (对 cppreference.com 页面进行一个很好的更改,以更直接地说明基于值的移动和复制/移动省略。)
-fno-elide-constructors
),也会调用移动构造函数。-fno-elide-constructors
不会禁用复制省略,它会禁用返回值优化。前者是你不能“禁用”的语言规则;后者是利用此规则的优化。事实上,我的全部观点是,即使不使用返回值优化,您仍然可以使用移动语义,这是同一组语言规则的一部分。-fno-elide-constructors
上的 GCC documentation:“C++ 标准允许实现省略创建仅用于初始化相同类型的另一个对象的临时对象。指定此选项会禁用该优化,并强制 G++ 调用复制构造函数情况。此选项还会导致 G++ 调用普通成员函数,否则这些函数将被内联扩展。在 C++17 中,编译器需要忽略这些临时变量,但此选项仍会影响普通成员函数。-fno-elide-constructors
编译选项禁用 all 复制省略,即用于返回语句 glvalue/prvalue 对象初始化器(这些复制省略分别称为 NRVO/RVO) ,变量prvalue对象初始化器,throw表达式glvalue对象初始化器和catch子句glvalue对象初始化器。从 C++ 17 开始,return 语句纯右值对象初始化器和变量纯右值对象初始化器必须使用复制省略,因此该选项现在仅在其余情况下禁用复制省略。