10

I am trying to understand the implementation of move-constructor. We all know if we need to manage resources in a C++ class, we need to implement Rule of five (C++ programming).

Microsoft gives us an example: https://msdn.microsoft.com/en-us/library/dd293665.aspx

Here is better one, which uses copy-swap to avoid code duplication: Dynamically allocating an array of objects

     // C++11
     A(A&& src) noexcept
         : mSize(0)
         , mArray(NULL)
     {
         // Can we write src.swap(*this);
         // or (*this).swap(src);
         (*this) = std::move(src); // Implements in terms of assignment
     }

In the move-constructor, directly:

         // Can we write src.swap(*this);
         // or (*this).swap(src);

Because I think (*this) = std::move(src) is a little more complicated. Because if we write as (*this) = src inadvertently, it would call normal assignment operator instead of the move-assignment-operator.

Apart from this question, in Microsoft's example, they wrote code like this: in move-assignment-operator, do we need to check self-assignment? Is it possible to happen?

// Move assignment operator.
MemoryBlock& operator=(MemoryBlock&& other)
{
   std::cout << "In operator=(MemoryBlock&&). length = " 
             << other._length << "." << std::endl;

   if (this != &other)
   {
      // Free the existing resource.
      delete[] _data;

      // Copy the data pointer and its length from the 
      // source object.
      _data = other._data;
      _length = other._length;

      // Release the data pointer from the source object so that
      // the destructor does not free the memory multiple times.
      other._data = nullptr;
      other._length = 0;
   }
   return *this;
}
Community
  • 1
  • 1
Dongguo
  • 379
  • 2
  • 4
  • 14
  • 1
    The move constructor seems silly to me. Why not just initialize everything in the initializer list and then set `src` to a valid moved from state? That should be at most 4 assignments and 3 if you don't care about changing the size of the moved from object. – NathanOliver Apr 04 '16 at 14:31
  • 1
    `In the move-constructor, directly: // Can we write src.swap(*this); // or (*this).swap(src);` I don't see why not, assuming you have a suitable `swap` method implemented. – Igor Tandetnik Apr 04 '16 at 14:32
  • Self-move-assignment can sometimes happen, specifically in sorting algorithms that do `swap(*it1, *it2)` it's possible that the two iterators could refer to the same element, which would do a self-swap, possibly leading to a self-move. If you've overloaded `swap(MemoryBlock&, MemoryBlock&)` then that shouldn't be a problem, a your specialized swap will be called instead of `std::swap`. Self-move should not happen otherwise, although it's still theoretically possible. – Jonathan Wakely Apr 04 '16 at 14:38
  • @JonathanWakely Interesting point, I have never checked if `this == &other` in assignment operators because I assumed that self assignment can only happen by mistake and that mistake should not be masked rather be discovered as soon as possible. – Maxim Egorushkin Apr 04 '16 at 17:18
  • 1
    @MaximEgorushkin: I forgot to link to http://wg21.link/lwg2468 – Jonathan Wakely Apr 04 '16 at 17:56
  • @NathanOliver, you are right. It is written in this way to avoid code duplication. It is easier to maintain. – Dongguo Apr 05 '16 at 01:11
  • @Igor Tandetnik, thanks :) – Dongguo Apr 05 '16 at 01:11

2 Answers2

10

One way is to implement the default constructor, copy constructor and swap function.

And then implement the move constructor, copy and move assignment operators using the first three.

E.g.:

struct X
{
    X();
    X(X const&);
    void swap(X&) noexcept;

    X(X&& b)
        : X() // delegate to the default constructor
    {
        b.swap(*this);
    }

    // Note that this operator implements both copy and move assignments.
    // It accepts its argument by value, which invokes the appropriate (copy or move) constructor.
    X& operator=(X b) {
        b.swap(*this);
        return *this;
    }
};

If you have been using this idiom in C++98, then once you add the move constructor you get the move assignment without writing a single line of code.

In some cases this idiom may be not the most efficient. Because the copy operator always first constructs a temporary and then swaps with it. By hand coding the assignment operators it may be possible to get better performance. When in doubt, check optimized assembly output and use a profiler.

Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
6

I'm also looking over the Internet to find the best way to implement move constructor and move assignment. There are a few approaches but neither are perfect.

Following are my findings so far.

Here is a Test class I'm using as an example:

class Test {
private:
  std::string name_;
  void*       handle_ = nullptr;

public:
  Test(std::string name)
    : name_(std::move(name))
    , handle_(malloc(128))
  {
  }
    
  ~Test()
  {
    if(handle_) free(handle_);
  } 

  Test(Test&& other) noexcept;             // we are going to implement it
  Test& operator=(Test&& other) noexcept;  // we are going to implement it

  void swap(Test& v) noexcept
  {
    std::swap(this->handle_, v.handle_);
    std::swap(this->name_, v.name_);
  }

private:
  friend void swap(Test& v1, Test& v2) noexcept
  {
    v1.swap(v2);
  }
};

Approach #1: Straight'n'forward

Test::Test(Test&& other) noexcept
  : handle_(std::exchange(other.handle_, nullptr))
  , name_(std::move(other.name_))
{
}

Test& Test::operator=(Test&& other) noexcept
{
  if(handle_) free(handle_);
      
  handle_ = std::exchange(other.handle_, nullptr);
  name_ = std::move(other.name_);

  return *this;
}

Pros

  • The best performance

Cons

  • Code duplication in move constructor and move assignment
  • Partially the code of destructor is duplicated in the move assignment

Approach #2: Destruction + Construction

Test::Test(Test&& other) noexcept
  : handle_(std::exchange(other.handle_, nullptr))
  , name_(std::move(other.name_))
{
}

Test& Test::operator=(Test&& other) noexcept
{
  this->~Test();
  new (this) Test(std::move(other));

  return *this;
}

Pros

  • No code duplication
  • Good performance in case if there are no virtual functions

Cons

  • Virtual Methods Table (VMT) is initialied twice (if class has virtual functions)
  • Cannot be used in the base class. Base class must implement move constructor only.

Approach #3: Copy'n'Swap

Test::Test(Test&& other) noexcept
  : handle_(std::exchange(other.handle_, nullptr))
  , name_(std::move(other.name_))
{
}

Test& Test::operator=(Test&& other) noexcept
{
  Test (std::move(other)).swap(*this);
  return *this;
}

or copy and move operators 2-in-1:

Test& Test::operator=(Test other) noexcept
{
  swap(other, *this);
  return *this;
}

Pros

  • No code duplication

Cons

  • Extra object is created
  • Extra smaller objects created while swapping data members inside the swap function
  • Some kind of code duplication in the swap function

Approach #4: Move constructor via move-assignment

That is what you @Dongguo have found on MSDN

Test::Test(Test&& other) noexcept
{
  *this = std::move(other);
}

Test& Test::operator=(Test&& other) noexcept
{
  if(handle_) free(handle_);
      
  handle_ = std::exchange(other.handle_, nullptr);
  name_ = std::move(other.name_);

  return *this;
}

Pros

  • No code duplication

Cons

  • Does not work for classes that contain the non-default constructible data members.
  • Data members are initialized twice in the move constructor

Links

More answers

  • All of your code has memory leaks! – mada Mar 26 '22 at 19:44
  • @0x0 Checked all the 4 solutions. Neither has a leak. Where is it? – Dmytro Ovdiienko Mar 27 '22 at 20:26
  • I think you somehow edited the code. Check out the first approach, in the move assignment, before this line, `handle_ = std::exchange(other.handle_, nullptr);` the `handle_` should be deleted because it's pointing to a temporary `Test` and you just orphan this memory by assigning it to `nullptr`. – mada Mar 27 '22 at 20:55
  • Or simply put your code in MCVS and run any leak checker as `_CrtDumpMemoryLeaks()` – mada Mar 27 '22 at 20:57
  • @0x0 I did not change the code significantly. Just replaced `close_handle` with `malloc/free` (in order to test for memory leaks). The rest of code did not change. See the history of changes. I used MSVC Address Sanitizer and Visual Leak Detector. Also checked `_CrtDumpMemoryLeaks` - there're no leaks. The code is here [https://pastebin.com/Q6rhkLXq] – Dmytro Ovdiienko Mar 28 '22 at 09:47