Move semantics defined how the data from one object can be moved to another object. In other words, it defines if it is possible to change the ownership on data from one object to another.

For example, if a class type variable is initialized with another object of the same type and if the type supports move semantic, the data would be moved to the target variable. This saves the copy (see copy semantic) that would be made otherwise.

When move semantics is invoked, if data member can be moved is moved and data which can’t be moved is copied.

When move semantic is invoked

When an object is initialized (or assigned) with value of same type, the move semantic will be invoked if:

  1. the type supports move semantics
  2. the initialization (or assignment) value is a rvalue (or temporary object).
  3. the move isn’t elided.

By the way, not all type support move semantics. However, std::vector and std:string both support.

As std::vector supports move semantics, we can return vectors by value from the function. For example,

std::vector<int> generateList()
{
    std::vector items{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    return items;
}
 
std::vector<int> items{generateList()};

Creating a class supporting move semantic

We have seen the problem with the smart pointer example in this note where the SmartPointer does not support move semantic. We can make it support move semantic by overwriting the copy constructor and overloading assignment operator.

For example:

#include <iostream>
#include <string>
#include <string_view>
 
class Person
{
    int m_age{};
    std::string m_name{};
 
public:
    Person(int age, std::string_view name)
        : m_age{age}, m_name{name}
    {
        std::cout << "Creating person" << '\n';
    }
 
    ~Person()
    {
        std::cout << "Destroying person" << '\n';
    }
 
    void sayHello() const
    {
        std::cout << "Hi" << '\n';
    }
 
    friend std::ostream &operator<<(std::ostream &out, const Person &p)
    {
        return out << "Name: " << p.m_name << "\nAge: " << p.m_age << "\n";
    }
};
 
class SmartPointer
{
    Person *m_ptr{};
 
public:
    SmartPointer(Person *ptr)
        : m_ptr{ptr} {}
 
    SmartPointer(SmartPointer &other)
    {
        m_ptr = other.m_ptr;
        other.m_ptr = nullptr;
    }
 
    ~SmartPointer()
    {
        delete m_ptr;
    }
 
    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;
    }
 
    operator bool()
    {
        return m_ptr != nullptr;
    }
};
 
void passByValue(SmartPointer ptr)
{
}
 
int main(int argc, char const *argv[])
{
 
    SmartPointer p{new Person(12, "Hemant")};
 
    {
        SmartPointer q{p};
    }
 
    std::cout << *p << '\n';
 
    p->sayHello();
    return 0;
}
 

With this, when we initialized q with p, we are basically transferring ownership of pointer from p to q using copy constructor. Overloaded assignment operator is also doing the same thing with copy constructor but with few extra things.

Problem with our move semantic

There is a very big issue with move semantic approach is that when it transfers the ownership, the original owner is pointing to null pointer. When the inner block finishes and control goes back to the outer scope, doing any operation on original object would result in unexpected behavior (program crash!!).

Also, the way we delete the deallocate the memory is by using delete m_ptr which is a non-array delete method. It does not work well with dynamic arrays.

Additionally, there is no distinction between copy semantic and move semantic when assignment or initialization happens using copy constructor:

SmartPointer q{p};
 
// or
 
q = p;

C++98 has std::auto_ptr which is a smart pointer same as SmartPointer and persists the same problems. Thus, it got deprecated in C++11 and remove in C++17. So, in C++11, move semantic was properly defined and std::auto_ptr was replaced with “move-aware” smart pointers, std::unique_ptr, std::weak_ptr, and std::shared_ptr. Out of these, the first and the last one are popular.

Tip

I have tried to solve one problem with our SmartPointer where it reassigns the pointer to original owner when its object gets destroyed. You can see the rough implementation here.

References

  1. https://www.learncpp.com/cpp-tutorial/returning-stdvector-and-an-introduction-to-move-semantics/
  2. https://www.learncpp.com/cpp-tutorial/introduction-to-smart-pointers-move-semantics/