The Standard doesn't not require a compiler to perform return-value-optimization(RVO), but then, since C++11, the result must be moved.
It looks as if, this might introduce UB to/break code, which was valid in C++98.
For example:
#include <vector>
#include <iostream>
typedef std::vector<int> Vec;
struct Manager{
Vec& vec;
Manager(Vec& vec_): vec(vec_){}
~Manager(){
//vec[0]=42; for UB
vec.at(0)=42;
}
};
Vec create(){
Vec a(1,21);
Manager m(a);
return a;
}
int main(){
std::cout<<create().at(0)<<std::endl;
}
When compiled with gcc (or clang for that matter) with -O2 -fno-inline -fno-elide-constructors
(I'm using std::vector
with these build-option, in order to simplify the example. One could trigger the same behavior without these options with handmade-classes and a more complicated create
-function) everything is OK for C++98(-std=c++98
):
return a;
triggers copy-constructor, which leaves a
intact.m
is called (must happens before a
is destructed, because m
is constructed after a
). Accessing a
in destructor is unproblematic.a
is called.The result is as expected: 21
is printed (here live).
The situation is however different when built as C++11(-std=c++11
):
return a;
triggers move-constructor, which "destroys" a
.m
is called, but now accessing a
is problematic, because a
was moved and no longer intact.vec.at(0)
throws now.Here is a live-demonstration.
Am I missing something and the example is problematic in C++98 as well?
A move constructor enables the resources owned by an rvalue object to be moved into an lvalue without copying.
To correct this, remove the move constructor completely. In the case of the class, once a copy constructor is present (user defined), the move is implicitly not generated anyway (move constructor and move assignment operator).
If a copy constructor, copy-assignment operator, move constructor, move-assignment operator, or destructor is explicitly declared, then: No move constructor is automatically generated. No move-assignment operator is automatically generated.
std::move is actually just a request to move and if the type of the object has not a move constructor/assign-operator defined or generated the move operation will fall back to a copy.
This is not a breaking change. Your code was already doomed in C++98. Imagine you have instead
int main(){
Vec v;
Manager m(v);
}
In the above example you access the vector when m
is destroyed and since the vector is empty you throw an exception (have UB if you use []
). This is essential the same scenario you get into when you return vec
from create
.
This means that your destructor should not be making assumptions about the state of its class members since it doesn't know what state they are in. To make your destructor "safe" for any version of C++ you either need to put the call to at
in a try-catch block or you need to test the size of the vector to make sure it is equal to or greater than what you expected.
"I just cannot believe valid code gets invalid..." Yes, it really may get invalid. Another example:
#include <iostream>
#include <string>
using namespace std;
template <typename T>
int stoi(const basic_string<T>& str)
{
return 0;
}
int main()
{
std::string s("-1");
int i = stoi(s);
std::cout << s[i];
}
The code is valid in C++98/03, but has UB in C++11.
The point is that the code like this or yours are extremal cases, which generally does/should not emerge in practice. They (almost) always represent a very bad coding practice. If you follow good coding habits, you likely won't get into any troubles when moving from C++98 to C++11.
Your code exposes various behavior depending on whether the RVO is applied (compiled without -fno-elide-constructors
) or with creating a temporary to return the result (with -fno-elide-constructors
).
With RVO the result is the same for C++98 and C++11 and it is 42. But introducing a temporary will hide the final assignment to 42 in C++98 and the function will return the result 21. In the C++11 version the things go even further as the temporary is created with move
semantics so the assignment to a moved (so empty) object will result in an exception.
The takeaway lesson is just to avoid putting any code with side effects in destructors and constructors as well for this matter.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With