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

2

42

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?

Gawlas answered 27/6, 2014 at 5:37 Comment(6)
1. Why do you believe that swap must be implemented using move? I do not see that in the standard. 2. I don't see the assignment to self in your swap implementation!?Brewington
@WernerHenze: 1. Per 20.2.2 of the Standard, the requirement for swappable types is that they must be MoveConstructible and MoveAssignable. 2. In the call std::swap(x, x), the parameters lhs and rhs are the same object, so lhs = std::move(rhs) is an assignment to self.Gawlas
You are right about 2., I missed that. Regarding 1.: the standard does not give an implementation, so I do not think that this is a defect in the standard but in the implementation. The implementation could check if &lhs==&rhs and then skip the move.Brewington
DR1204 which you linked to is talking about move-assignment operators for standard library implementations. That doesn't apply as a general statement to user-defined classes.Cocke
BTW, it does not matter when self-move-assignment damages the value in your swap implementation. After the self-assignment, there is a move-assignment to not-self which resurrects the original value, provided that such not-self-assignments do not damage the value.Flowering
is this supposed to be language-lawyer?Suite
T
5

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

Therapeutics answered 21/5, 2021 at 8:37 Comment(0)
S
2

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.

Scilicet answered 18/6, 2022 at 10:19 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.