I've been experimenting with the following C++ code using different compiler flags and versions, observing differing behaviors in object construction between C++11
and C++17
.
I noticed that with the -fno-elide-constructors
flag in both C++17
and C++11
, NRVO
is disabled inside the function f
. However, in C++17, the object t1
is created directly at the location of t2
, but in C++11, this step wasn't eliminated. I'm wondering why in C++17, despite disabling that flag, this step was still eliminated, considering this is not a case of mandatory copy elision!
Also I checked a lot of sources here and those famous questions and couldn't figure it out why!
Her is also link to the code: https://godbolt.org/z/PYfejba6M
#include <iostream>
struct Thing
{
Thing() { std::cout << "Constructor\n"; }
~Thing() { std::cout << "Destructor\n"; }
Thing(const Thing&) { std::cout << "Copy Constructor\n"; }
Thing& operator=(const Thing&) { std::cout << "Copy Assignment Operator\n"; return *this; }
Thing(Thing&&) noexcept { std::cout << "Move Constructor\n"; }
Thing& operator=(Thing&&) noexcept { std::cout << "Move Assignment Operator\n"; return *this; }
};
Thing f()
{
std::cout << "Entering f()\n";
Thing t1;
std::cout << "Thing t1 constructed in f()\n";
std::cout << "Preparing to return t1 from f()\n";
return t1;
}
int main()
{
std::cout << "Entering main()\n";
[[maybe_unused]] Thing t2 = f();
std::cout << "t2 object returned and assigned in main()\n";
std::cout << "Exiting main()\n";
}
So, first off, when you say "in C++17, the object t1
is created directly at the location of t2
", you're not describing the behavior of your code correctly. In your own godbolt link, with -std=c++17 -fno-elide-constructors
, you'll note it still shows one Move constructor
output, and a paired Destructor
output. t1
was not created directly at the location of t2
. With -std=c++11 -fno-elide-constructors
, you get two Move constructor
outputs, so C++17 is eliding one of two moves even with the flag, but not both.
The reason is that -fno-elide-constructors
disables optional copy-elision. NRVO is optional in both standards, so regardless of C++ standard, it does, in fact, cause a single move to occur when you do return t1;
in f
, moving from the t1
inside the function to the "staging area" for the return value.
In C++11, this "staging area" is separate from the t2
that is initialized by the function call; the copy-elision from the "staging area" to t2
is non-mandatory (because C++11 lacks mandatory copy-elision) and is disabled by -fno-elide-constructors
, so you end up moving twice, once from t1
to staging area, once from staging area to t2
.
By contrast, in C++17 and higher, the copy from "staging area" to t2
is a case where copy-elision is mandatory. So while -fno-elide-constructors
disables the non-mandatory copy-elision from t1
to the "staging area", the "staging area" itself is required to be t2
itself (because Thing t2 = f();
requires elision), so only one move occurs, directly from t1
to t2
.