Home » C++ » Is `x = std::move(x)` undefined?

Is `x = std::move(x)` undefined?

Posted by: admin November 30, 2017 Leave a comment

Questions:

Let x be a variable of some type that has been previously initialized. Is the following line:

x = std::move(x)

undefined? Where is this in the standard and what does it say about it?

Answers:

No, this is not undefined behavior, it is going to be implementation defined behavior, it will depend on how move assignment is implemented.

Relevant to this is LWG issue 2468: Self-move-assignment of library types , note this is an active issue and does not have an official proposal so this should be considered indicative rather than definitive, but it does point out the sections that are involved for the standard library and points out they currently conflict. It says:

Suppose we write

vector<string> v{"a", "b", "c", "d"};
v = move(v);

What should be the state of v be? The standard doesn’t say anything
specific about self-move-assignment. There’s relevant text in several
parts of the standard, and it’s not clear how to reconcile them.

[…]

It’s not clear from the text how to put these pieces together, because it’s not clear which one takes precedence. Maybe 17.6.4.9 [res.on.arguments] wins (it imposes an implicit precondition that isn’t mentioned in the MoveAssignable requirements, so v = move(v) is undefined), or maybe 23.2.1 [container.requirements.general] wins (it explicitly gives additional guarantees for Container::operator= beyond what’s guaranteed for library functions in general, so v = move(v) is a no-op), or maybe something else.

On the existing implementations that I checked, for what it’s worth, v = move(v) appeared to clear the vector; it didn’t leave the vector unchanged and it didn’t cause a crash.

and proposes:

Informally: change the MoveAssignable and Container requirements tables (and any other requirements tables that mention move assignment, if any) to make it explicit that x = move(x) is defined behavior and it leaves x in a valid but unspecified state. That’s probably not what the standard says today, but it’s probably what we intended and it’s consistent with what we’ve told users and with what implementations actually do.

Note, for built-in types this is basically a copy, we can see from draft C++14 standard section 5.17 [expr.ass]:

In simple assignment (=), the value of the expression replaces that of the object referred to by the left
operand.

which is different than the case for classes, where 5.17 says:

If the left operand is of class type, the class shall be complete. Assignment to objects of a class is defined
by the copy/move assignment operator (12.8, 13.5.3).

Note, clang has a self move warning.

Questions:
Answers:

It will call X::operator = (X&&), so it is up to the implementation to manage this case (as it is done for X::operator = (const X&))

Questions:
Answers:

All that does is call X::operator=(X&&) (with an lvalue qualified “*this“).

On primitive types, std::move does little of interest, and does not interact with = at all. So this only applies to objects of class type.

Now, for a type within std (or generated by one of its templates), objects moved from tend to be left in an unspecified (yet valid) state. This is not undefined behavior, but it isn’t useful behavior.

The semantics of a each given X::operator=(X&&) would have to be examined, examining every type within std would be “too broad” for a stack overflow answer. They even may contradict themselves.

In general, when moveing from an object, you are communicating to the consumer that “you don’t care what state the object is in afterwards”. The use of a x = std::move(x) is thus impolite, as you (usually) do care what state x is in after the operation completes (as you are assigning to it). You are using the same object as both an lvalue and an rvalue within the same operation, and that is not good a practice.

An interesting exception is the default std::swap, which goes something like this:

template<class T>
void swap(T& lhs, T& rhs) {
  T tmp = std::move(lhs);
  lhs = std::move(rhs);
  rhs = std::move(tmp);
}

the middle line, lhs = std::move(rhs), does an x = std::move(x) if you call swap twice on the same object.

Note, however, that we do not care what state x is in after this line is completed; we have already stored the state of x in tmp, and we will restore it on the next line.