Self-exchange leaves us in a valid but unspecified state
C++14 introduced std::exchange() to the standard utility library.
We can use it to replace the argument with a new value and, optionally, return its previous value.
For example:
auto obj = std::string{ "hello" };
auto const old = std::exchange(obj, "goodbye");
assert(old == "hello"); // `old` contains the initial value of `obj`; and
assert(obj == "goodbye"); // we have (ex)changed the value of `obj`
What happens if we exchange an object with itself?
auto str = std::string{ "hello" };
std::exchange(str, str);
assert(str == "");
str appears to have been emptied, which is also its default state. We could assume that a self-exchange "resets" an object to its initial state.
A std::string is more-or-less a std::vector<char>,
so why not empty a vector with std::exchange()?
auto vec = std::vector<char>{ 'h', 'e', 'l', 'l', 'o' };
std::exchange(vec, vec);
assert(vec == std::vector<char>{});
How about a single char?
auto c = 'c';
std::exchange(c, c);
assert(c == 'c');
A char is not reset to its default value when exchanged with itself.
It is an integral type, and those are not default-initialised,
so it makes sense that it cannot be "reset".
We can define a custom struct and default initialise its member(s).
Are such objects "reset" when exchanged with themselves?
struct S
{
int x{ 0 };
private:
friend auto operator==(S const& lhs, S const& rhs) -> bool
{
return lhs.x == rhs.x;
}
};
auto s = S{ 42 };
std::exchange(s, s);
assert(s == S{ 42 });
It appears that some types “reset” the object, while others leave it untouched, but all of the above are valid states for their respective types.
std::exchange() is defined
in the section utility.exchange
as:
template<class T, class U = T> T exchange(T& obj, U&& new_value);
// Equivalent to:
T old_value = std::move(obj);
obj = std::forward<U>(new_value);
return old_value;
A lot has been said about move semantics, their complexity, and the pitfalls of std::move().
I have found Klaus Igleberger's
two
part
introduction to move semantics to be worthwhile
and it appears to be a regular topic in CppCon's "Back to Basics" track.
The short answer that we are interested here is that trivially copyable types (scalars, classes that follow the rule-of-zero, or arrays of those) are always copied and thus do not implement move semantics. Move semantics on standard library types specifically leave the object in a valid but unspecified state: the object must behave as intended, but its value is implementation-defined.
Note that I have not yet described the implementation(s) used for all the assertions above. I used Compiler Explorer to test with Clang, GCC, and MSVC (ARM and x86, all the way back to C++14). It appears that the main compiler implementations have conformed to the same behaviour, as shown above.
However, do remember that the implementations are free to change this in the future. Just because they might behave identically now, does not mean they will always do so. If you want to use this in production, please assert the expected behaviour. Otherwise, you might be surprised when updating your infrastructure.