- Rewrite article: less prescriptive style, explicitly discourage usage, and briefly explain move semantics
We can clear a C++ std::string with std::exchange()
auto str = std::string{ "hello" };
auto const old = std::exchange(str, str);
assert(str == "");
assert(old == "hello");
std::exchange() was introduced to the standard library with C++14
to replace the argument with a new value and
return the previously held content.
The trick shown above,
to clear a value by self-exchange
is not the intended usage of that utility function.
We can also clear a std::vector<> via std::exchange(),
but should also not.
In fact, it seems that we can clear a lot of standard library objects via std::exchange():
std::string(),
std::vector<>,
std::filesystem::path, and
std::shared_ptr<>,
to name a few.
However,
others
such as
fundamental types,
data-structs,
simple classes, or
std::array<>,
all leave the initial object untouched and usable in future statements.
For example:
auto c = '@';
auto const old = std::exchange(c, c);
assert(c == '@');
assert(old == '@');
I tried multiple compilers and standards with Compiler Explorer and all seem to behave similarly. I do not know if this is intentional, emergent behaviour from other standard rules, or undefined behaviour.
Curious to learn more, I peeked into the standard.
The definition of std::exchange(),
in the section utility.exchange,
indicates that it takes advantage of move semantics:
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;
Move semantics are intended to avoid copying expensive objects.
We are supposed to use std::move()
to cast an object into a unnamed object (U&&),
so that it can be used in in overload resolutions for move constructors, or assignments
(as is the case of T old_value above).
It is those move constructors and assignments that define what happens to an object in its moved-from state.
Notably, std::move() itself does not actually move any data.
The standard does explain the difference in behaviour
when self-exchanging
an object with itself.
Trivially copyable types
such as scalars, classes that follow the rule-of-zero, or arrays of those,
do not implement move semantics and are thus always copied.
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.
A lot has been said about the complexity of move semantics
and the pitfalls of std::move().
I have found Klaus Igleberger's
two
part
introduction to be worthwhile,
should you be interested in learning more.
I do not know when it would come in handy, to exchange an object with itself, nor do I recommend using it in production. I do find that behaviour rather cheeky, it always brings a smile to my face, and I wanted to share it with you :)