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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
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 static not_a_pod create( char const * data_ = "TEST" ) noexcept { return { _strdup( data_) }; } static 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. Let us do the simple value move first
1 2 3 4 5 |
not_a_pod specimen = not_a_pod::create(); not_a_pod rezult = std::move(specimen); printf( "\n%s", rezult.data); // free(): double free detected in tcache 2 // or simillar crash |
The result is an application crash because of an Access violation. Let’s repeat the above but with comments
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/// specimen contains a pointer to char /// to heap allocated mem block not_a_pod specimen = not_a_pod::create(); /// calling move constructor with the /// result of std::move not_a_pod rezult = std::move(specimen); /// specimen instance is moved but it contains a pointer that is not moved /// thys both internal pointers are now pointing to the same /// heap allocated mem block /// that will be freed in 2 compiler generated destructors /// of two instances upon application exit /// free(): double free detected in tcache 2 |
The cause: 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. So how do we stop that and make not_a_pod
movable?
C++20 has a concept std::movebale
which immediately revel the requirements. Thus we need to add the move constructor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/// inside not_a_pod const char* data{}; not_a_pod( const char * ptr ) : data( ptr) {} not_a_pod() = default; /// move ctor provokes the need for /// default constructor /// + the need for local swap imp. not_a_pod(not_a_pod && other) noexcept { /// this is a construcotr, data is null at this moment /// in case of pointers std::swap does the pointer rewiring std::swap( data, other.data ); /// signal to the other destrucotor not to free the data /// we are also pointing to other.data = nullptr; } // both arguments can not be const // because we change the internal of both to do the swapping friend void swap(not_a_pod & left, not_a_pod & right ) noexcept { // remember, std::swap calls std::move on both arguments // that in turn call move constructors on both of them std::swap( left.data, right.data ); } |
Which as we see, in turn, provoked the need for two constructors, move constructor and the swap()
friend, where we implement data pointers rewiring.
Basically, we have added a user-defined move constructor, swap and two constructors. And move worketh after these changes. As did the value swapping too.
1 2 3 4 5 6 7 8 9 |
not_a_pod left = not_a_pod::create("LEFT"); not_a_pod right = not_a_pod::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() |
Data has stayed where it was. We are just pointing to it from the object moved to, and detach the point for the object moved from.
Above using
is the standard idiom to pacify the ADL. What about pointer tests? Let’s try and move the pointer.
1 2 3 4 5 6 7 |
not_a_pod* specimen = not_a_pod::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
1 2 3 4 5 |
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 of implementation that is swappable. Please observe what scaffolding we had to implement to have moved off our type of work.
There is the fully functioning Godbolt implementation. Please copy it into your favourite IDE, compile and follow through the debugger. At least few times. Compiled with all the warnings included, as C++17
and with maximal optimizations. For GCC or clang that means: -std=c++17 -Wall -O3
.
Conclusion
C++ terminology is important and hard to grasp immediately. 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.
Appendix A
(please go to next page)