Search code examples
c++c++11rvalue-referencemove-semantics

Providing correct move semantics


I am currently trying to figure out how to do move semantics correctly with an object which contains a pointer to allocated memory. I have a big datastructure, which contains an internal raw pointer to the actual storage (for efficiency reasons). Now I added a move constructor and move operator=(). In these methods I am std::move()ing the pointer to the new structure. However I am not sure what to do with the pointer from the other structure.

Here is a simple example of what I am doing:

class big_and_complicated {
   // lots of complicated code
};

class structure {
public:
   structure() :
      m_data( new big_and_complicated() )
   {}

   structure( structure && rhs ) :
      m_data( std::move( rhs.m_data ) )
   {
      // Maybe do something to rhs here?
   }

   ~structure()
   {
      delete m_data;
   }

private:
   big_and_complicated * m_data;
}

int main() {
  structure s1;
  structure s2( std::move( s1 ) );
  return 0;
}

Now from what I understand, after the std::move( s1 ) to s2 the only thing that is safe to do on s1 ist to call its constructor. However as far as I can see, this would lead to deleting the pointer contained within s1 in the destructor, rendering s2 useless as well. So I am guessing I have to do something to render the destructor safe when std::move()ing the pointer. As far as I can see the safest thing to do here, is to set it to 0 in the moved object, since this would turn the delete into a no-op later on. Is this reasoning correct so far? Or is std::move() actually smart enough to null out the pointer for me, rendering its usage safe? So far I am seeing no crashes in the actual test-suite, but I have not made sure the move-constructor is actually called.


Solution

  • "Moving" a pointer is no different than copying one and does not set the moved-from value to null ('moving' is in quotes here because std::move does not move anything really, it just changes the value category of the argument). Just copy rhs's pointer then set it to nullptr:

    struct structure
    {
        structure()
          : m_data{new big_and_complicated{}}
        { }
    
        structure(structure&& rhs)
          : m_data{rhs.m_data}
        {
            rhs.m_data = nullptr;
        }
    
        structure& operator =(structure&& rhs)
        {
            if (this != &rhs)
            {
                delete m_data;
                m_data = rhs.m_data;
                rhs.m_data = nullptr;
            }
            return *this;
        }
    
        ~structure()
        {
            delete m_data;
        }
    
    private:
        big_and_complicated* m_data;
    
        structure(structure const&) = delete;             // for exposition only
        structure& operator =(structure const&) = delete; // for exposition only
    }
    

    You could also simplify this by using std::exchange:

    structure(structure&& rhs)
        : m_data{ std::exchange(rhs.m_data, nullptr) }
    { }
    
    structure& operator=(structure&& rhs)
    {
        if (this != &rhs)
        {
            delete m_data;
            m_data = std::exchange(rhs.m_data, nullptr);
        }
        return *this;
    }
    

    Better yet, use std::unique_ptr<big_and_complicated> instead of big_and_complicated* and you don't need to define any of this yourself:

    #include <memory>
    
    struct structure
    {
        structure()
          : m_data{new big_and_complicated{}}
        { }
    
    private:
        std::unique_ptr<big_and_complicated> m_data;
    }
    

    Lastly, unless you actually want structure to remain non-copyable, you're better off just implementing proper move semantics inside of big_and_complicated and having structure hold a big_and_complicated object directly.