This question is based on discussion below a recent blog post by Scott Meyers.
It seems "obvious" that std::swap(x, x)
should leave x
unchanged in both C++98 and C++11, but I can't find any guarantee to that effect in either standard. C++98 defines std::swap
in terms of copy construction and copy assignment, while C++11 defines it in terms of move construction and move assignment, and this seems relevant, because in C++11 (and C++14), 17.6.4.9 says that move-assignment need not be self-assignment-safe:
If a function argument binds to an rvalue reference parameter, the implementation may assume that this parameter is a unique reference to this argument. ... [ Note: If a program casts an lvalue to an xvalue while passing that lvalue to a library function (e.g. by calling the function with the argument move(x)), the program is effectively asking that function to treat that lvalue as a temporary. The implementation is free to optimize away aliasing checks which might be needed if the argument was an lvalue. —end note ]
The defect report that gave rise to this wording makes the consequence clear:
this clarifies that move assignment operators need not perform the traditional if (this != &rhs) test commonly found (and needed) in copy assignment operators.
But in C++11 and C++14, std::swap
is expected to use this implementation,
template<typename T>
void swap(T& lhs, T& rhs)
{
auto temp(std::move(lhs));
lhs = std::move(rhs);
rhs = std::move(temp);
}
and the first assignment is performing an assignment to self where the argument is an rvalue. If the move assignment operator for T
follows the policy of the standard library and doesn't worry about assignment to self, this would seem to court undefined behavior, and that would mean that std::swap(x, x)
would have UB, as well.
That's worrisome even in isolation, but if we assume that std::swap(x, x)
was supposed to be safe in C++98, it also means that C++11/14's std::swap
could silently break C++98 code.
So is std::swap(x, x)
guaranteed to leave x
unchanged? In C++98? In C++11? If it is, how does this interact with 17.6.4.9's permission for move-assignment to not be self-assignment-safe?
I believe this may be covered, at least in C++20(a), by the [utility.requirements]
section which states:
15.5.3.2
describes the requirements on swappable types and swappable expressions.
That referenced section, [swappable.requirements]
, further states (my emphasis in the final two bullet points):
An object
t
is swappable with an objectu
if and only if:
- the expressions
swap(t, u)
andswap(u, t)
are valid when evaluated in the context described below; and- these expressions have the following effects:
- the object referred to by
t
has the value originally held byu
; and- the object referred to by
u
has the value originally held byt
.
It seems to me that, if self-swap somehow damaged the contents, those bolded sections would be invalidated, meaning that they wouldn't be swappable.
That same section also later states:
An rvalue or lvalue
t
is swappable if and only ift
is swappable with any rvalue or lvalue, respectively, of typeT
.
There's no waffling around with self-swaps there, it clearly states any rvalue or lvalue (respectively), including itself.
(a) Both these constraints also exist in C++17, c++14, and C++11, anything older than that, I don't really care about :-)
TLDR: Yes, std::swap(x, x)
is always fine (for any x of MoveAssignable type T).
As I understand, self-move-assignment might leave the object in a valid but unspecified state, so it's not UB, and, as mentioned by @j6t in the comment, std::swap
brings back the original value, so it should work.
In this Q&A Howard Hinnant writes in the comment:
swap(v, v) is legal and required to work. And it typically does because the self-move-assignment is done with a moved-from value (assuming we're not talking about vector which has a specialized swap which is also required to work with self-swap). In the generic self-swap, you're move assigning from an unspecified value to an unspecified value. So it really doesn't matter what happens as long as it doesn't crash. swap(t, t) requires T to be MoveAssignable. That requirement holds even if T is in a moved-from state.
As I understand, this is (finally! in the upcoming C++23) covered by LWG 2839: Self-move-assignment of library types, again (which came after LWG 2468: Self-move-assignment of library types).
2839 contains the following:
An object of a type defined in the C++ standard library may be move-assigned (11.4.6 [class.copy.assign]) to itself. Such an assignment places the object in a valid but unspecified state unless otherwise specified.
The part of the standard you quoted now looks like:
If a function argument is bound to an rvalue reference parameter, the implementation may assume that this parameter is a unique reference to this argument, except that the argument passed to a move-assignment operator may be a reference to *this ([lib.types.movedfrom]).
I.e. it seems to have an exception specifically for self-move-assignment to not be UB.
See also Eric Niebler's article and the SO answer it's based on. In the answer Howard Hinnant (lead author of libc++) writes that:
Some will argue that swap(x, x) is a good idea, or just a necessary evil. And this, if the swap goes to the default swap, can cause a self-move-assignment.
I disagree that swap(x, x) is ever a good idea. If found in my own code, I will consider it a performance bug and fix it.
The Core Guidelines however have a point about allowing self-move-assignment in user-defined types. But that's just an advice (which e.g. the standard library apparently doesn't adhere to). It mentions:
Note The ISO standard guarantees only a “valid but unspecified” state for the standard-library containers. Apparently this has not been a problem in about 10 years of experimental and production use. Please contact the editors if you find a counter example. The rule here is more caution and insists on complete safety.
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