Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is std::swap(x, x) guaranteed to leave x unchanged?

Tags:

c++

c++11

swap

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?

like image 645
KnowItAllWannabe Avatar asked Jun 27 '14 05:06

KnowItAllWannabe


2 Answers

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 object u if and only if:

  • the expressions swap(t, u) and swap(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 by u; and
    • the object referred to by u has the value originally held by t.

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 if t is swappable with any rvalue or lvalue, respectively, of type T.

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

like image 185
paxdiablo Avatar answered Sep 23 '22 18:09

paxdiablo


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.

like image 1
deshalder Avatar answered Sep 24 '22 18:09

deshalder