C++ the good parts — std::swap

Non-Trivial User-Defined Types


Swapping Non-Trivial User-Defined Types

As you know for the simple pod-like type we have used above, the compiler has generated all the necessary scaffolding, constructors and the rest. And the whole section was almost boring?

Let us create and use the simple but nontrivial type to analyze moving and swapping in scenarios where native pointers are the members.

struct not_a_pod final {
// native pointer data member
// set to to nullptr
char* data{};
// destructor will free
// the allocated storage
 ~not_a_pod() {
   if (data) {
    free (data);
    data = nullptr;
/// helper factory functions
inline not_a_pod create( char const * data_ = "TEST" ) noexcept 
{ return { _strdup( data_) }; }

inline not_a_pod* create_ptr(char const* data_ = "TEST" ) noexcept
{ return new not_a_pod{ create(data_) }; }
}; // not_a_pod

Remember: we have left it to the trusty C++ compiler to generate implicit destructors and assignment operators. All six of them. Good. Lets do the simple value move first

not_a_pod specimen = create();
not_a_pod rezult = std::move(specimen);
_ASSERTE(specimen.data == rezult.data);

The result is an application crash because of Access violation. Let’s repeat the above but with comments

/// specimen contains a pointer to char
/// to heap allocated mem block
not_a_pod specimen = create();
/// calling move constructor with the
/// result of std::move
not_a_pod rezult = std::move(specimen);
/// both data pointers are now pointing to the same 
/// heap allocated mem block
/// that will be freed in 2 compiler generated destructors 
/// of two instances
/// and that will provoke an Access violation crash
_ASSERTE(specimen.data == rezult.data);

Implicitly generated move ctor moved over just the pointer from the specimen, not the actual heap-allocated data.  Thus resulting in two pointers to the same memory.  Thus the second free from the second destructor provoked a crash.

Not to worry. Let’s add the move constructor.

/// inside not_a_pod
not_a_pod() = default;
/// move ctor
/// having this provokes the need for 
/// default constructor
/// + the need for local swap imp.
not_a_pod(not_a_pod && other) noexcept
/// pointer rewiring
 std::swap( data, other.data );
/// signal to the other not to free the data
/// we are also pointing to
 other.data = nullptr;

friend void swap(not_a_pod & left, not_a_pod & right ) noexcept 
   std::swap( left.data, right.data );

Which as we see, in turn, provoked the need for default ctor and for the swap()  friend, where we implement data pointers rewiring.

Basically we have added user-defined move constructor, swap and default constructor. And move value worketh after these changes. As did the value swapping too.

not_a_pod left = create("LEFT");
not_a_pod right = create("RIGHT");
/// having move ctor
/// provoked the need for default ctor
/// and local swap
/// why this using here?
using std::swap;
swap(left, right);
/// remember the synopsis of swap()

Above using is the standard idiom to pacify the ADL.  What about pointer tests? Let’s try and move the pointer.

not_a_pod* specimen = create_ptr();
not_a_pod* rezult = std::move(specimen);
// pointer moving returns the same pointer
// as expected
_ASSERTE(specimen == rezult);
// do not erase twice!
delete specimen;

And swapping the pointers worketh too

not_a_pod* left = create_ptr("LEFT");
 not_a_pod* right = create_ptr("RIGHT");
   std::iter_swap(left, right);
 delete left;
delete right;

That is an absolutely minimal non-trivial type implementation that is swappable. Please observe what scaffolding we had to implement to have moved off our type of work.


Terminology is important and hard to grasp. Flexibility brings complexity. Modern C++ is very flexible. That means your move implementation can be very finely tuned for non-trivial types you create…

Modern C++ is built on a few of these mechanisms. I do hope by reading this long post you gained a better understanding of the intricacies of swapping, moving and value-based semantics.