Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Move constructor for returned objects breaks C++98 code?

Tags:

c++

gcc

c++11

rvo

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):

  1. return a; triggers copy-constructor, which leaves a intact.
  2. Destructor of m is called (must happens before a is destructed, because m is constructed after a). Accessing a in destructor is unproblematic.
  3. Destructor of 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):

  1. return a; triggers move-constructor, which "destroys" a.
  2. Destructor of m is called, but now accessing a is problematic, because a was moved and no longer intact.
  3. vec.at(0) throws now.

Here is a live-demonstration.

Am I missing something and the example is problematic in C++98 as well?

like image 985
ead Avatar asked Apr 24 '19 11:04

ead


People also ask

What is the use of move constructor in C++?

A move constructor enables the resources owned by an rvalue object to be moved into an lvalue without copying.

How do I turn off move constructor?

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).

Are move constructor automatically generated?

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.

Does STD move move constructor?

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.


3 Answers

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.

like image 65
NathanOliver Avatar answered Oct 17 '22 21:10

NathanOliver


"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.

like image 34
Daniel Langr Avatar answered Oct 17 '22 23:10

Daniel Langr


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.

like image 1
jszpilewski Avatar answered Oct 17 '22 22:10

jszpilewski