6

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
Andreas DM
  • 10,685
  • 6
  • 35
  • 62
Vegeta
  • 461
  • 2
  • 6
  • [Not reproducible](https://godbolt.org/z/cbhefz7MK). Works with C++17 as expected. – n. m. could be an AI Jun 18 '21 at 11:44
  • It is error in c++20. I have given a godbolt example. https://godbolt.org/z/E35M51Mbz – Vegeta Jun 18 '21 at 11:46
  • 2
    Older compiler versions compile this just fine with `-std=c++17` but newer ones fail. This is weird. – lubgr Jun 18 '21 at 11:48
  • Is there any problem with my code? I was playing around with NVRO. This problem suddenly came in. – Vegeta Jun 18 '21 at 11:50
  • Hm there seem to be two different errors, one in `auto v = f();` (up to C++14, not an error in C++17 and up) and one in `return v;` (gcc -std=c++17 and below doesn't catch this but clang does). – n. m. could be an AI Jun 18 '21 at 12:03
  • At any rate, NRVO is an optimisation. It is not mandatory. Validity of a program cannot change depending on whether it's implemented. – n. m. could be an AI Jun 18 '21 at 12:06
  • My question is why it is an error? It can fall back to copy as it is available? – Vegeta Jun 18 '21 at 12:08
  • fwiw also with `Box f()` and `Box v = f();` gcc issues the same error about deleted move constructor on the `return v;` – 463035818_is_not_an_ai Jun 18 '21 at 12:18
  • FWIK, the move constructor shouldn't even be considered eligible as it's marked deleted and the copy constructor should be used instead. This *seems* like a bug. Clang 11 compiled this code with `-std=c++17`, but not Clang 12. – Zoso Jun 18 '21 at 12:27
  • I thought a relevant rule changed in some C++ Standard version, but I can't find it now. – aschepler Jun 18 '21 at 12:39
  • "even when it takes place and the copy/move constructor is not called, it still must be present and accessible (as if no optimization happened at all), otherwise the program is ill-formed" - from https://en.cppreference.com/w/cpp/language/copy_elision – sklott Jun 18 '21 at 12:41
  • 1
    @Zoso A deleted move constructor is only skipped if it was automatically marked deleted (it was not declared, or was defined with `= default`, and a base class or non-static member can't be moved), not if it was declared using `= delete`. – aschepler Jun 18 '21 at 12:44
  • 2
    *"If marked deleted, why can't it simply use the copy constructor as it is defined"* When marked `= delete`, the move constructor still becomes a candidate for consideration. That's not the same as when the compiler is unable to synthesize the move constructor, in which case it would not be considered as a candidate. I wish I could do `= void` (or something) to explicitly provide "I intended for this not to be generated" without it being a candidate for matching consideration. Alas, a comment will have to do instead. – Eljay Jun 18 '21 at 12:47
  • @aschepler Ah, in that case, this example's move constructor would still be *eligible* and would explain the error. Could you point me to the reference that states this? – Zoso Jun 18 '21 at 12:48
  • @Vegeta I think [this](https://stackoverflow.com/a/24694027/1851678) answer explains why the move constructor is chosen even though it's marked deleted. – Zoso Jun 18 '21 at 12:54
  • 3
    @Zoso [\[over.match.funcs\]/9](https://timsong-cpp.github.io/cppwp/over.match.funcs#general-9) "A **defaulted** move special member function that is defined as deleted is excluded from the set of candidate functions in all contexts." – aschepler Jun 18 '21 at 13:02

2 Answers2

5

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.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
1

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]


Andreas DM
  • 10,685
  • 6
  • 35
  • 62