Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What is the rationale for self-assignment-unsafe move assignment operators in the standard library?

The standard library policy about move assignment is that the implementation is allowed to assume that self-assignment will never happen; this seems to me a really bad idea, given that:

  • the "regular" ("copy") assignment contract in C++ has always been regarded as safe against self-assignment; now we have yet another incoherent corner case of C++ to remember and to explain - and a subtly dangerous one, too; I think we all agree that what is needed in C++ is not more hidden traps;
  • it complicates algorithms - anything in the remove_if family need to take care of this corner case;
  • it would be really easy to fulfil this requirement - where you implement move with swap it comes for free, and even in other cases (where you can get some performance boost with ad-hoc logic) it's just a single, (almost) never taken branch, which is virtually free on any CPU¹; also, in most interesting cases (moves involving parameters or locals) the branch would be removed completely by the optimizer when inlining (which should happen almost always for "simple" move assignment operators).

So, why such a decision?


¹ Especially in library code, where implementers can liberally exploit compiler-specific hints about "branch expected outcome" (think __builtin_expect in gcc/__assume in VC++).

like image 461
Matteo Italia Avatar asked Oct 04 '16 12:10

Matteo Italia


2 Answers

Moved from objects in std are supposed to be discarded or assigned to prior to being reused. Anything that is not completely free beyond that is not promised.

Sometimes things are free. Like a move-constructed-from container is empty. Note that some move-assiged-from cases have no such guarantee, as some implementations may choose to move elements instead of buffers. Why the difference? One was a free extra guarantee, the other not.

A branch or other check is not completely free. It takes up a branch prediction slot, and even if predicted is merely almost free.

On top of that, a = std::move(a); is evidence of a logic error. Assign-from a (within std) means you will only assign-to or discard a. Yet here you are wanting it to have specific state on the next line. Either you know you are self-assigning, or you do not. If you do not, you are now moving from object you are also populating and you do not know it.

The principle of "do little things to keep things safe" clashes with "you do not pay for that which you do not use". In this case, the second won.

like image 115
Yakk - Adam Nevraumont Avatar answered Oct 17 '22 18:10

Yakk - Adam Nevraumont


Yakk gives a very good answer (as usual and upvoted) but this time I wanted to add just a little more information.

The policy on self-move-assignment has shifted a tiny bit over the past half-decade. We have just recently clarified this corner case in LWG 2468. Actually, I should be more precise: An informal group between meetings agreed to the resolution of this issue, and it is likely to be voted into the C++1z working draft next month (Nov 2016).

The gist of the issue is to modify the MoveAssignable requirements to clarify that if the target and source of a move assignment are the same object, then there are no requirements on the value of the object after the assignment (except that it must be a valid state). It further clarifies that if this object is being used with the std::lib, it must still meet the requirements of the algorithm (e.g. LessThanComparable) whether or not it was move-assigned or even self-move-assigned.

So...

T x, y;
x = std::move(y);  // The value of y is unspecified and x == the old y
x = std::move(x);  // The value of x is unspecified

But both x and y are still in valid states. No memory has been leaked. No undefined behavior has occurred.

Rationale For This Position

It is still performance. However it is recognized that swap(x, x) has been legal since C++98 and does occur in the wild. Furthermore since C++11 swap(x, x) performs a self move assignment on x:

T temp = std::move(x);
x = std::move(x);
x = std::move(temp);

Prior to C++11, swap(x, x) was (a rather expensive) no-op (using copy instead of move). LWG 2468 clarifies that with C++11 and after, swap(x, x) is still a (not quite as expensive) no-op (using move instead of copy).

Details:

T temp = std::move(x);
// temp now has the value of the original x, and x's value is unspecified
x = std::move(x);
// x's value is still unspecified
x = std::move(temp);
// x's value now has the value of temp, which is also the original x value

To accomplish this no-op, self-move-assignment on x can do anything it wants as long as it leaves x in a valid state without asserting or throwing an exception.

If you want to specify that for your type T self-move-assignment is a no-op, that is perfectly fine. The std::lib does exactly that for unique_ptr.

If you want to specify that for your type U self-move-assignment leaves it in a valid but unspecified state, that is also fine. The std::lib does exactly that for vector. Some implementations (I believe VS) go to the trouble to make self-move-assignment on vector a no-op. Other's don't (such as libc++).

like image 5
Howard Hinnant Avatar answered Oct 17 '22 18:10

Howard Hinnant