NRVO 什么时候开始?需要满足哪些要求?

Veg*_*eta 5 c++ copy-elision c++14 c++17 c++20

我有以下代码。

#include <iostream>    
struct Box {
        Box() { std::cout << "constructed at " << this << '\n'; }
        Box(Box const&) { puts("copy"); }
        Box(Box &&) = delete;
        ~Box() { std::cout << "destructed at " << this << '\n'; }
};
 
auto f() {
    Box v;
    return v; // is it eligible for NVRO?
}

int main() {
    auto v = f(); 
}
Run Code Online (Sandbox Code Playgroud)

上面的代码产生错误。 call to deleted constructor/function in both gcc and clang

但是,如果我更改代码以返回纯右值,则代码有效。

auto f() {
      Box v;
      return Box(); // is it because of copy elision? 
  }
   
Run Code Online (Sandbox Code Playgroud)

为什么会这样?是因为删除移动构造函数吗?如果我将复制和移动构造函数都更改为显式,它也会产生错误吗?

如果标记为已删除,为什么不能简单地使用定义的复制构造函数

编辑:

      compiled with -std=c++20 in both gcc and clang, error.
      compiled with -std=c++17 gcc, compiles.
      compiled with -std=c++17 clang, error.
Run Code Online (Sandbox Code Playgroud)

编辑2:

      clang version: 12.0.0
      gcc version:   11.1
Run Code Online (Sandbox Code Playgroud)

n. *_* m. 5

该程序中有两种不同的潜在错误。

auto v = f();在 C++14 及更低版本中是错误,因为该操作在逻辑上是移动构造,而在 C++17 及更高版本中不是错误,因为它是临时而不是移动构造的具体化。这是 C++17 保证的复制省略功能,与 NRVO 不同。

return v;在所有版本的 C++ 中都是错误,因为它在逻辑上是移动构造,并且构造函数需要存在且可访问。NRVO 大多数时候都会优化构造函数,但 NRVO 不是强制性的,它只是允许的,因此它不能使原本无效的程序变得有效。然而,gcc 并不能捕获这个错误std=c++17。相反,它会退回到复制构造函数。这似乎是一个 gcc 错误。

C++17 不强制要求 NRVO。当操作数是纯右值时,它要求在语句中进行复制省略return,因此在这种情况下不需要存在复制/移动构造函数。这就是为什么return Box();有效。


And*_* DM 1

显然 C++20 标准发生了变化,影响了复制/移动省略(引用草案):

\n
\n

受影响的子条款:[class.copy.elision]
更改:返回隐式可移动实体的函数可能会调用构造函数,该构造函数采用对与返回的表达式不同的类型的右值引用。可以抛出函数和 catch 子句参数使用移动构造函数。

\n
\n

以及给出的例子:

\n
struct base {\n  base();\n  base(base const &);\nprivate:\n  base(base &&);\n};\n\nstruct derived : base {};\n\nbase f(base b) {\n  throw b;                      // error: base(base &&) is private\n  derived d;\n  return d;                     // error: base(base &&) is private\n}\n
Run Code Online (Sandbox Code Playgroud)\n

以及[class.copy.elision]的要点(强调我的):

\n
\n

隐式可移动实体是具有自动存储持续时间的变量,它可以是非易失性对象,也可以是非易失性对象类型的右值引用。在以下复制初始化上下文中,\n在尝试复制操作之前首先考虑移动操作:

\n

如果 return ([stmt.return])\nor co_\xc2\xadreturn ([stmt.return.coroutine]) 语句中的表达式是一个(可能\n带括号的)id 表达式,它命名在中声明的隐式可移动实体\n最里面的函数或lambda 表达式的主体参数声明子句,或者

\n

如果 throw 表达式 ([expr.throw]) 的操作数是一个(可能带括号的)nid 表达式,该表达式命名属于不包含最内层复合语句的范围的隐式可移动实体ntry-block 或 function-try-block(如果有),其复合语句或\nctor-initializer 包含 throw 表达式、重载解析以\n选择复制的构造函数或调用的 return_\xc2\xadvalue 重载为首先执行时就好像表达式或操作数是\nrvalue。如果第一次重载决策失败或未执行,\n则再次执行重载决策,并将表达式 or\n操作数视为左值

\n

[注 3 :无论是否发生复制省略,都会执行此两阶段重载决策。如果未执行省略,它会确定要调用的构造函数或\nreturn_\xc2\xadvalue 重载,并且\n所选的构造函数或\nreturn_\xc2\xadvalue 重载必须是可访问的,\n即使调用已被省略。\xe2\x80\x94尾注]

\n
\n
\n