We have tried implementing move semantic using copy constructor and overloading assignment operator for copy and saw problems with this approach.

To solve this problems, C++11 provides move constructor and move assignment.

Move constructor and assignment

Move constructor and move assignment uses rvalue references to implement move semantic. This way, it solve the problem of implementing move semantic in copy constructor and copy assignment.

Let’s modify above example by adding move constructors and assignment operator.

#include <iostream>
#include <string>
#include <string_view>
 
class SmartPointer
{
    Person *m_ptr{};
 
public:
    SmartPointer()
        : m_ptr{nullptr} {}
 
    SmartPointer(Person *ptr)
        : m_ptr{ptr} {}
 
    // copy constructor
    SmartPointer(const SmartPointer &other)
    {
        m_ptr = new Person;
        *m_ptr = *other.m_ptr;
    }
 
    // move constructor
    // transfer other.m_ptr to m_ptr
    SmartPointer(SmartPointer &&other) noexcept
        : m_ptr{other.m_ptr}
    {
        std::cout << "Moving ownership" << '\n';
        other.m_ptr = nullptr; // required to avoid dangling pointers
    }
 
    ~SmartPointer()
    {
        delete m_ptr;
    }
 
    // copy assignment operator overload
    SmartPointer &operator=(const SmartPointer &other)
    {
        if (this == &other)
            return *this;
 
        delete m_ptr;
 
        m_ptr = new Person;
        *m_ptr = *other.m_ptr;
 
        return *this;
    }
 
    SmartPointer &operator=(SmartPointer &&other)
    {
        if (this == &other)
            return *this;
 
        delete m_ptr;
 
        m_ptr = other.m_ptr;
        other.m_ptr = nullptr;
 
        return *this;
    }
 
    Person &operator*() const
    {
        return *m_ptr;
    }
 
    Person *operator->() const
    {
        return m_ptr;
    }
};
 
SmartPointer getPerson()
{
    SmartPointer p{new Person()};
    return p;
};
 
int main(int argc, char const *argv[])
{
 
    SmartPointer p1;
 
    p1 = getPerson();
    std::cout << *p1 << '\n';
 
    p1->sayHello();
    return 0;
}

First try running this program and see if there are still copies happening.

On my machine, it shows following output:

Creating person
Moving ownership
Person()

Hi
Destroying person

Phew! No unnecessary copies are being made.

Note

I used -fno-elide-constructors flag with compile command so that compiler does not elide move constructor as well.

What is happening here? Let’s understand.

  1. Same as before, first Creating person is coming from dynamically creating Person in getPerson function.
  2. There where interesting thing happens. So, this time, the temporary object is not created using copy constructor, rather created using move constructor, so we are seeing Moving ownership log. However, we saw in rvalue references that rvalue reference can only be created for a rvalue but p is not a rvalue. This is a special rule by the compiler where lvalue being returned can be moved instead of copy.
  3. Now, in main function, getPerson returns the temporary object which then move assigned to p1.

Note

noexcept is used to mark that the function does not throw any exception.

How does it work?

When we were moving using copy constructor and copy assignment, we were moving source to target and so source was pointing nothing. In case where source is a temporary object, it is fine as temporary object would anyway be destroyed. However, if source is another object which will live for longer time and be accessed later, accessing a moved source would result in undefined behavior. For example:

SmartPointer p1{new Person()};
 
{
    SmartPointer p2{p2};
} // p2 gets destroyed here
 
p1.access(); // would fail.

In above example, we are assuming move is done using copy constructor and copy assignment.

Now, with move constructor and assignment, as they accept only rvalues, rvalues are literal values and temporary objects, when there is a copy or assignment from a temporary object, the move will happen. Otherwise, copy will happen.

Implicit move constructor and assignment operator

Implicit move constructor and assignment operator are added by the compiler only when:

  1. There is no user defined copy constructor and assignment operator.
  2. There is no user defined move constructor and assignment operator.
  3. And there is no user defined destructor.

Implicit move constructor and assignment operator will do member wise move by calling move constructor and assignment of the member if present, otherwise copying the member.

Delete copy constructor and assignment

By deleting those member functions, we can make SmartPointer only movable.

class SmartPointer
{
//...other...
 
    // copy constructor
    SmartPointer(const SmartPointer &other) = delete;
 
    // copy assignment operator overload
    SmartPointer &operator=(const SmartPointer &other) = delete;
 
//...other...
};
 
void printPerson(const SmartPointer &ptr)
{
    std::cout << *ptr << '\n';
}
 
int main(int argc, char const *argv[])
{
 
    SmartPointer p{new Person()};
 
    printPerson(p);
    p->sayHello();
    return 0;
}

We can only pass SmartPointer by lvalue reference, which is a good thing to do.

This way, our SmartPointer is finally a smart pointer like std::unique_ptr.

Move with copy-and-swap idiom

Similar to the issues with copy assignment operator that we solved using copy-and-swap idiom, we use this method for move assignment operator.

For example:

class SmartPointer
{
    // ... 
    SmartPointer &operator=(SmartPointer &&other)
    {
        if (this == &other)
            return *this;
 
        delete m_ptr;
 
        m_ptr = other.m_ptr;
        other.m_ptr = nullptr;
 
        return *this;
    }
 
    friend void swap(SmartPointer &lhs, SmartPointer &rhs)
    {
        using std::swap;
 
        swap(lhs.m_ptr, rhs.m_ptr);
    }
    // ...

References

  1. https://www.learncpp.com/cpp-tutorial/move-constructors-and-move-assignment/