- Initial publication

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