C++中带括号的返回语句中的nrvo/copy elision

7
我正在研究以下代码,并使用我的Visual Studio 2017应用程序和两个不同的在线编译器得到了不同的结果。在发布模式下,Visual Studio在两种情况下都会省略复制/移动操作,而两个在线编译器只会在未加括号的返回语句中省略。我的问题是:谁是正确的,更重要的是,底层规则是什么。(我知道您可以与decltype(auto)语法一起使用括号。但这不是当前的用例。)
示例代码:
#include <iostream>
#include <cstdio>

struct Foo
{
    Foo() { std::cout << "default constructor" << std::endl; }
    Foo(const Foo& rhs) { std::cout << "copy constructor" << std::endl; }
    Foo(Foo&& rhs) { std::cout << "move constructor" << std::endl; }
    Foo& operator=(const Foo& rhs) { std::cout << "copy assignment" << std::endl; return *this; }
    Foo& operator=(Foo&& rhs) { std::cout << "move assignment" << std::endl; return *this; }
};

Foo foo_normal()
{
    Foo a{};
    return a;
}

Foo foo_parentheses()
{
    Foo a{};
    return (a);
}

int main()
{
    auto a = foo_normal();
    auto b = foo_parentheses();
    std::getchar();
}

在线编译器1: http://cpp.sh/75bux

在线编译器2: http://coliru.stacked-crooked.com/a/c266852b9e1712f3

在Visual Studio中以发布模式输出的结果为:

default constructor
default constructor

在另外两个编译器中,输出结果为:
default constructor
default constructor
move constructor

这让我感到惊讶。我以为C++17需要复制省略,并且符合规范的实现不允许打印“移动构造函数”。 - nwp
看起来gcc理解(a)不是object的名称。也许将其标记为[tag:language-lawyer]。 - nwp
@Someprogrammerdude,有一些区别,但我猜在日常编程中并不重要。例如https://dev59.com/Tm445IYBdhLWcg3wpL1_#25615981。使用情况涉及到`decltype(auto)`。 - phön
@nwp 是的,我也很惊讶,但也许对于括号表达式有特殊(转换)规则。我现在意识到第一个编译器仅支持c++14。 - phön
3个回答

8
这是标准中相关的引用
引用: 所谓复制省略,即 copy elision,在以下情况下被允许(可以组合使用以消除多个复制): (1.1) - 在返回类型为类的函数的返回语句中,当表达式是非易失性自动对象(不是函数参数或由处理程序的异常声明引入的变量([except.handle])),且具有与函数返回类型相同的类型(忽略cv-qualification)时,可以通过直接将自动对象构造到函数调用的返回对象中来省略复制/移动操作。
因此要求如下:
  1. 在返回语句中
  2. 在函数中
  3. 返回类型为类
  4. 当表达式是非易失性自动对象的名称(不是函数参数或由处理程序的异常声明引入的变量([except.handle]))时
  5. 具有与函数返回类型相同的类型(忽略cv-qualification)
我认为要求1、2、3和5已经满足,但是要求4没有被满足。 (a) 不是一个对象的名称。因此,在给定的代码中,不适用复制省略。由于移动构造函数具有副作用,因此它也不能根据类似规则省略。
因此,gcc 是正确的,visual studio(以及 clang)在这里是错误的。

(a) 不是一个对象的名称。https://timsong-cpp.github.io/cppwp/n4868/expr.prim.paren#1.sentence-2 - undefined
@LanguageLawyer - 即使假设该条款适用,编译器在这种情况下也不是“必须”进行拷贝省略;而是“允许”。据我所知,只有在(它本来就被允许并且)变量名包含子字符串“yesPleaseActuallyCopyElide”或类似荒谬的情况下,编译器才被允许进行拷贝省略。 - undefined

5

GCC 是正确的。

根据 [class.copy.elision] 段落 1

在以下情况下(可以组合以消除多个副本),允许省略拷贝/移动操作,称为复制省略

  • 在具有类返回类型的函数中,当表达式是非易失性自动对象的名称(不是函数参数或由处理程序([except.handle])的异常声明引入的变量)与函数返回类型相同类型(忽略cv限定)时,在return语句中,可以通过将自动对象直接构造到函数调用的返回对象中来省略拷贝/移动操作。

  • ...

带括号的表达式在return语句中不符合复制省略的条件。

实际上,在解决CWG 1597之前,return语句中带括号的id表达式甚至不能被视为一个右值以执行移动操作。


但为什么它不符合标准呢?仅仅因为名称在括号中吗?这是一个简单的规则吗?我曾经在某个地方读到过,表达式的评估将导致lvalue,但我对此并不了解。最终对我来说似乎很奇怪。 - phön
return语句中的括号表达式不符合复制省略的条件。请参考链接:https://timsong-cpp.github.io/cppwp/n4868/expr.prim.paren#1.sentence-2 - undefined

3

这里是P1155 "更多隐式移动"P2266 "更简单的隐式移动"的作者。

简短回答:这是GCC的一个bug。现在已经修复了(在GCC 11和GCC 12之间)。

法律专家的回答可能是,return (x);是否应该触发复制省略仍然不清楚。但实际上,我认为所有供应商现在都在同一页面上。

[class.copy.elision] 的所有内容只是说,“……当 表达式 是非易失对象的名称时……” 你可能会认为,显然一个对象的名称不能包含括号——但是,另一方面,如果我们真的想要说“字面上指代变量的标识符”,我们有一个非常成熟的术语来描述它:我们会说“……当 表达式 是命名了非易失对象的id-expression时……”而我们并没有这样说。

同时,在 C++20 和 C++23 中,我们更加谨慎地清理了“隐式移动”的措辞。[expr.prim.id.unqual]/4 现在(在 C++23 中)说:

在以下情况下,id-expression 可以被视为 移动可选
  • (4.1) 如果 id-expression(可能带括号)returnco_return 语句的操作数...
  • (4.2) 如果 id-expression(可能带括号)throw-expression 的操作数...
因此,这意味着 return (x); 百分之百会经历 "隐式移动"。由于开发商知道这一点,他们可以正确理解,在可能时它也应该进行复制省略。

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