【问题标题】:When does NRVO kick in? What are the requirements to be satisfied?NRVO 什么时候开始?要满足哪些要求?
【发布时间】:2021-09-03 03:21:01
【问题描述】:

我有以下代码。

#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(); 
}

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

但如果我将代码更改为返回纯右值,代码就可以工作。

auto f() {
      Box v;
      return Box(); // is it because of copy elision? 
  }
   

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

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

编辑:

      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.

编辑 2:

      clang version: 12.0.0
      gcc version:   11.1

【问题讨论】:

  • Not reproducible。按预期使用 C++17。
  • 较旧的编译器版本可以使用 -std=c++17 很好地编译它,但较新的编译器版本会失败。这很奇怪。
  • @Zoso 删除的移动构造函数仅在自动标记为已删除(未声明,或使用= default 定义,并且不能是基类或非静态成员)时才被跳过已移动),而不是使用 = delete 声明的。
  • "如果标记为已删除,为什么不能简单地使用定义的复制构造函数" 当标记为= delete时,移动构造函数仍然成为考虑的候选对象.这与编译器无法合成移动构造函数时不同,在这种情况下,它不会被视为候选者。我希望我可以做= void(或其他东西)来明确提供“我打算不生成这个”,而不会将它作为匹配考虑的候选者。唉,只能用评论代替。
  • @Zoso [over.match.funcs]/9 "A defaulted 将定义为已删除的特殊成员函数从所有上下文中的候选函数集中排除。"

标签: c++ c++17 c++14 c++20 copy-elision


【解决方案1】:

这个程序有两个不同的潜在错误。

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(); 起作用的原因。

【讨论】:

  • return v; is an error in all versions of C++ because it is logically a move construction。为什么它逻辑上是一个移动结构?
  • @Zoso 还会是什么?
  • v 本身并不是prvalue。如果移动构造函数不存在,它将是一个副本,不是吗?
  • @Zoso 是的,但是 move 存在(即使它被明确删除),所以它被考虑并选择了。
  • 伙计,我会不断遇到更多奇怪的错误。尽管看起来可以理解,但从技术上讲这是一个错误
【解决方案2】:

显然 C++20 标准中发生了影响复制/移动省略的更改 (Quoting the draft):

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

给出的例子:

struct base {
  base();
  base(base const &);
private:
  base(base &&);
};

struct derived : base {};

base f(base b) {
  throw b;                      // error: base(base &&) is private
  derived d;
  return d;                     // error: base(base &&) is private
}

还有来自[class.copy.elision] 的要点(强调我的):

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

如果表达式中的 return ([stmt.return]) 或 co_return ([stmt.return.coroutine]) 语句是一个(可能 带括号的)id 表达式,用于命名 隐式可移动实体 bodyparameter-declaration-clause中声明 封闭函数或 lambda 表达式,或

如果操作数 一个 throw 表达式 ([expr.throw]) 是一个 (可能是带括号的) id-expression 命名一个隐式可移动实体,该实体属于 一个不包含最里面的复合语句的范围 try-block 或 function-try-block(如果有),其复合语句或 ctor-initializer 包含抛出表达式,重载决议到 选择复制的构造函数或 return_value 重载到 首先执行调用,就好像表达式或操作数是 右值。如果第一个重载决议失败或没有执行, 再次执行重载决议,考虑表达式或 操作数作为左值

[注 3:无论在 是否会发生复制省略。它决定了构造函数还是 如果不执行省略,则调用 return_value 重载,和 选定的构造函数或return_value重载必须可访问 即使呼叫被省略。 — 尾注]


【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-09-15
    • 2017-11-27
    相关资源
    最近更新 更多