When does NRVO kick in? What are the requirements to be satisfied?

Viewed 414

I have the following code.

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

The above code produces error. call to deleted constructor/function in both gcc and clang

But If I change the code to return prvalue, the code works.

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

Why is this happening? Is it because of delete move constructor? If I change both copy and move constructors to explicit, it also produces error?

If marked deleted, why can't it simply use the copy constructor as it is defined

Edit:

      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.

Edit 2:

      clang version: 12.0.0
      gcc version:   11.1
2 Answers

There are two different potential errors in this program.

auto v = f(); is an error in C++14 and below because the operation is logically a move construction, and not an error in C++17 and up because it is a materialisation of a temporary rather than a move construction. This is the guaranteed copy elision feature of C++17, which is distinct from NRVO.

return v; is an error in all versions of C++ because it is logically a move construction, and the constructor needs to be present and accessible. NRVO optimises the constructor away most of the time, but NRVO is not mandatory, it is merely allowed, so it cannot make an otherwise invalid program valid. However, gcc does not catch this error with std=c++17 and lower. It falls back to the copy constructor instead. This seems to be a gcc bug.

C++17 does not mandate NRVO. It mandates copy elision in the return statement when the operand is a prvalue, so in this case a copy/move constructor need not be present. This is why return Box(); works.

Apparently there was a change in the C++20 standard that affects copy/move elision (Quoting the draft):

Affected subclause: [class.copy.elision]
Change: A function returning an implicitly movable entity may invoke a constructor taking an rvalue reference to a type different from that of the returned expression. Function and catch-clause parameters can be thrown using move constructors.

And the example that is given:

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
}

And the takeaway from [class.copy.elision] (emphasis mine):

An implicitly movable entity is a variable of automatic storage duration that is either a non-volatile object or an rvalue reference to a non-volatile object type. In the following copy-initialization contexts, a move operation is first considered before attempting a copy operation:

If the expression in a return ([stmt.return]) or co_­return ([stmt.return.coroutine]) statement is a (possibly parenthesized) id-expression that names an implicitly movable entity declared in the body or parameter-declaration-clause of the innermost enclosing function or lambda-expression, or

if the operand of a throw-expression ([expr.throw]) is a (possibly parenthesized) id-expression that names an implicitly movable entity that belongs to a scope that does not contain the compound-statement of the innermost try-block or function-try-block (if any) whose compound-statement or ctor-initializer contains the throw-expression, overload resolution to select the constructor for the copy or the return_­value overload to call is first performed as if the expression or operand were an rvalue. If the first overload resolution fails or was not performed, overload resolution is performed again, considering the expression or operand as an lvalue.

[Note 3: This two-stage overload resolution is performed regardless of whether copy elision will occur. It determines the constructor or the return_­value overload to be called if elision is not performed, and the selected constructor or return_­value overload must be accessible even if the call is elided. — end note]


Related