ChatGPT解决这个技术问题 Extra ChatGPT

c++11返回值优化还是搬家? [复制]

这个问题在这里已经有了答案: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 );
}

我应该使用哪个?

从我目前所读到的内容来看,普遍的共识似乎是依赖于使用 RVO 的编译器,而不是明确地使用 move:现代编译器足够聪明,几乎可以在任何地方使用 RVO,而且它比 move 更有效。但这只是“传闻”,请注意,所以我对记录在案的解释非常感兴趣。
对于局部变量函数的返回值,您永远不需要显式移动。这是隐含的举动。
然后编译器可以自由选择:如果可能,它将使用 RVO,如果没有,它仍然可以执行移动(如果该类型无法移动,那么它将执行复制)。
@MartinBa,永远不要说永不;)如果局部变量与返回类型的类型不同,则需要显式移动,例如std::unique_ptr<base> f() { auto p = std::make_unique<derived>(); p->foo(); return p; },但如果类型相同,它会尽可能移动(并且移动可能是省略)
为了完整起见,@JonathanWakely 所说的内容已在缺陷报告中得到解决,并且至少最新版本的 gcc 和 clang 不需要在那里明确移动。

K
Kerrek SB

只使用第一种方法:

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

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

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


即使禁用复制省略 (-fno-elide-constructors),也会调用移动构造函数。
@Maggyero:-fno-elide-constructors 不会禁用复制省略,它会禁用返回值优化。前者是你不能“禁用”的语言规则;后者是利用此规则的优化。事实上,我的全部观点是,即使不使用返回值优化,您仍然可以使用移动语义,这是同一组语言规则的一部分。
-fno-elide-constructors 上的 GCC documentation:“C++ 标准允许实现省略创建仅用于初始化相同类型的另一个对象的临时对象。指定此选项会禁用该优化,并强制 G++ 调用复制构造函数情况。此选项还会导致 G++ 调用普通成员函数,否则这些函数将被内联扩展。在 C++17 中,编译器需要忽略这些临时变量,但此选项仍会影响普通成员函数。
@Maggyero:听起来像是文档中的一个错误,特别是,听起来文档的措辞没有针对 C++11 进行更新。提交错误? @JonathanWakely?
在 C++ 17(C++ 11 和 C++ 14)之前,-fno-elide-constructors 编译选项禁用 all 复制省略,即用于返回语句 glvalue/prvalue 对象初始化器(这些复制省略分别称为 NRVO/RVO) ,变量prvalue对象初始化器,throw表达式glvalue对象初始化器和catch子句glvalue对象初始化器。从 C++ 17 开始,return 语句纯右值对象初始化器和变量纯右值对象初始化器必须使用复制省略,因此该选项现在仅在其余情况下禁用复制省略。
f
fury.slay

所有返回值要么已经是 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 限定的),则再次执行重载决议,将对象视为左值。


我不是特别喜欢整个“编译器可以做 X”的论点。这个问题不需要求助于任何编译器。这纯粹是关于语言。对于“移动”是否发生,没有什么“可选”或模糊的。该语言非常清楚哪些类型的构造函数参数可以绑定到返回值(这是一个 xvalue);重载决议完成其余的工作。
这与编译器可以做什么无关,而是主要编译器可以做什么。显式地移动东西可能会妨碍编译器做事甚至比移动更好。任何先进到足以让您显式移动的编译器几乎肯定是先进到足以自动移动返回值 - 因为与您可能想要显式移动的其他情况不同,返回值很容易被编译器检测为一个好地方进行优化(因为任何返回都是保证该值不会在执行返回的函数中进一步使用)。
@Damon:嗯,有点。它编译器可以移动返回值(并保存副本),但他们通常不会。相反,他们尽可能使用复制 ellison,这样可以节省复制和移动。它们只是直接分配给接收函数结果的变量,而不是返回并稍后分配的临时变量。手动移动变量永远不会比编译器做得更好,而且通常会稍微(只是稍微)差一点。编译器依赖于移动语义,但尽可能使用 RVO。至少,这是我的理解。
所有返回值都已被移动或优化” 如果类型不匹配,则不是:groups.google.com/a/isocpp.org/forum/#!msg/std-proposals/…
@cdyson37 有趣的是,我以前从未遇到过这种极端情况。幸运的是,如果没有 std::move(),它甚至无法编译。我试图弄清楚该示例是否实际上展示了预期语言功能的一部分,或者利用了模板化成员函数的意外怪癖(在这种情况下,std::unique_ptr() 的模板化移动构造函数)。
O
Oktalist

这很简单。

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 语句中的右值引用已隐式移出。


非常重要的例外,谢谢。刚刚在我的代码中遇到了这个。
对于编程语言来说,这是一个多么有趣的状态,人们必须使用记忆助记符来编码决策树,以了解如何做一件简单的事情,比如在没有副本的情况下返回一个值。移动语义和右值是否普遍认为是 cpp 设计的成功?对于在我看来是一个简单问题的问题,它们当然是一个复杂的解决方案。再加上 NVRO 的隐含使用,这肯定会导致设计非常混乱。
@ldog,至于许多设计决策,不仅只关注c++,而且几乎总是在利弊之间取得平衡。当考虑到右值引用的所有优点时,以这种方式手动抑制 RVO/NRVO 对我来说似乎是可以接受的风险,特别是如果错误是通过 return std::move(....自 C++11 以来,右值函数参数是该语言的新参数,现有的先前代码或“既定风格习惯”不会被意外破坏。自 C++17 以来保证复制省略进一步有助于记住这里的事情。
A
Adam H. Peterson

如果您要返回一个局部变量,请不要使用 move()。这将允许编译器使用 NRVO,否则,编译器仍将被允许执行移动(局部变量在 return 语句中变为 R 值)。在该上下文中使用 move() 只会禁止 NRVO 并强制编译器使用移动(如果移动不可用,则使用副本)。如果您返回的不是局部变量,NRVO 无论如何都不是一个选项,如果(且仅当)您打算窃取该对象时,您应该使用 move()


那是对的吗?如果我重用以下示例:en.cppreference.com/w/cpp/language/copy_elision 在 return 语句中添加 std::move(第 17 行),不会禁用复制省略。该标准实际上说复制省略将省略“std::move”和复制构造函数。
@ThomasLegris,我不明白你的评论。如果您在谈论 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,这就是我的观点。
@ThomasLegris,好的,我明白你所看到的,但我有另一种解释。确实正在执行移动,但移动的是 vector<Noisy> 而不是 Noisyvector<> 的移动构造函数可以通过指针操作移动包含的对象,因此不必移动单个对象。如果您将函数更改为直接使用 Noisy 而不是 vector<Noisy>,则会显示移动。
@ThomasLegris,如果您有兴趣,在该示例中查看移动操作的另一种方法是将 vector<Noisy> 替换为 array<Noisy,3>。这使您可以结合对象容器查看移动,但对象直接作为值聚合到数据类型中,而不是隐藏在允许 STL 优化掩盖移动的 freestore 分配后面。 (对 cppreference.com 页面进行一个很好的更改,以更直接地说明基于值的移动和复制/移动省略。)