6

I tried the following on gcc 13.1 on C++ on C++11/17/20/23 but it fails to compile when the move or copy constructor is deleted.

If those constructors are not deleted, then named return value optimization works, and neither copy/move are done.

Interestingly enough, if I remove the name, and return the prvalue directly then the plain return value optimization works.

Can anyone provide an explanation for this?

#include <memory>
#include <iostream>

struct Foo{
    Foo(int v): a{v} { std::cout << "Create!\n"; }
    ~Foo() { std::cout << "Destruct!\n"; }


    Foo(const Foo&)=delete;
    Foo(Foo&&)=delete;
    
    int a;
};

// I DON'T WORK!
Foo makeFoo() {
    Foo foo{5};
    return foo;
}

// I WORK!
//Foo makeFoo() {
//    return Foo{5};
//}

int main() {
    auto foo = makeFoo();
    std::cout << "Hello world! " << foo.a << "\n";
}
Aamir
  • 1,974
  • 1
  • 14
  • 18
Nathan Doromal
  • 3,437
  • 2
  • 24
  • 25
  • 5
    Copy elision is mandatory for RVO since C++17. That means, compiler must do it and it will never call any copy/move operations. NRVO however is still just an optional optimisation, so compiler must ensure that your variable can be returned validly (via copy/move) - even if it eventually optimises the copy/move entirely. – Yksisarvinen Jul 19 '23 at 21:22
  • 2
    Does this answer your question? [What are copy elision and return value optimization?](https://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization) – Yksisarvinen Jul 19 '23 at 21:24
  • When the optimization is *optional*, the code must be valid even if the option is not used. – BoP Jul 20 '23 at 07:35

1 Answers1

5

Although copy elision is indeed mandatory from C++17 on, there's a reason why you must still provide a move constructor when returning a named object from a function by value.

This is because it is possible to write code where NVRO is not possible. Here's a simple example:

std::string foo (bool b)
{
    std::string s1 = "hello";
    std::string s2 = "world";
    return (b) ? s1 : s2;
}

Now NVRO works by allocating memory for the object to be returned at the call site, and then doing, essentially, a placement new when the object is constructed in foo.

But with the code above the compiler can't do that (not in the general case anyway) because there are two possible objects it might return, and it doesn't know, ahead of time, which one it will be. So it is forced to move-construct the returned string from either s1 or s2, depending on what's passed in b.

Now you could argue that in code where the compiler doesn't face this problem, NVRO should not require a viable move constructor. But the committee evidently decided that what is and is not permissible would then be too confusing, and I agree with them. Let's face it, life is hard enough for compiler writers already.

Paul Sanders
  • 24,133
  • 4
  • 26
  • 48