Home > other >  std::move Bug Spotted in C Primer 5th Edition
std::move Bug Spotted in C Primer 5th Edition

Time:12-26

I looked at an example in C Primer explaining std::move. The example is as follows:

int &&rr1 = 42;
int &&rr3 = std::move(rr1);

In the explanation of the above code snippet, it is written that:

Calling move tells the compiler that we have an lvalue that we want to treat as if it were an rvalue. It is essential to realize that the call to move promises that we do not intend to use rr1 again except to assign to it or to destroy it. After a call to move, we cannot make any assumptions about the value of the moved-from object.

My first question is that: Can i now safely write std:: cout << rr1;? Since in the above quote paragraph, it is written that we cannot make any assumptions about the value of the moved-from object so is it guaranteed by the standard that std::cout << rr1; is safe(not UB or depends on implementation etc)?

Here is another example,

std::string str = "abc"; 
auto&& str2 = std::move(str); 
cout << str; // is this safe(not UB or depends on implementation)

Similarly, in the above snippet, is it safe to use the value of str like cout << str;?

If yes, then is this a mistake/bug in the book C Primer by Stanley.

CodePudding user response:

std::move itself doesn't do anything but add a rvalue reference, so both examples are safe.

Neither std::move nor access through a rvalue reference have any special impact on the validity or state of the object in the core language.

The purpose of std::move is only to be used as argument to e.g. function calls, as a promise to the callee that you won't care about the state of the object after the call.

If the function e.g. doesn't modify the state of the object, then it is also well-defined to use the object's state after passing it to the function with std::move. There is no undefined behavior because of the std::move call itself in any case.

So the book is correct, that a use of std::move should generally be treated as a promise to not rely on the state of the object after the call. It is just a bit unclear that the promise is just a contract, not part of the core language. It is towards callees, not towards the compiler.

But in particular the standard library relies on this promise in general. When the rest of the calling code then falsely relies on the state after a standard library call, this may result in undefined behavior. Because of the special position of the standard library, the promise is in this case also towards the compiler.

The details of the contract differ. For example std::unique_ptr makes strong guarantees after a move. The following is well-defined and the assert condition is always satisfied:

auto p = std::make_unique<int>(1);
auto q = std::move(p);
auto ptr = p.get();
assert(ptr == nullptr);

It is simply a standard convention that without such specific guarantees, a move operation may result in any valid state of the passed object.

CodePudding user response:

Both cases are safe because no move operation (move construction or move assignment) happens. For example, in auto&& str2 = std::move(str);, std::move(str) just produces an xvalue (rvalue) and then it's bound to the reference str2.

In particular, std::move produces an xvalue expression that identifies its argument t. It is exactly equivalent to a static_cast to an rvalue reference type.

On the other hand,

std::string str = "abc"; 
auto str2 = std::move(str); // move construction happens

Here's the example from cppreference.com:

Unless otherwise specified, all standard library objects that have been moved from are placed in a "valid but unspecified state", meaning the object's class invariants hold (so functions without preconditions, such as the assignment operator, can be safely used on the object after it was moved from):

std::vector<std::string> v;
std::string str = "example";
v.push_back(std::move(str)); // str is now valid but unspecified
str.back(); // undefined behavior if size() == 0: back() has a precondition !empty()
if (!str.empty())
    str.back(); // OK, empty() has no precondition and back() precondition is met
 
str.clear(); // OK, clear() has no preconditions
  • Related