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-constructorsflag with compile command so that compiler does not elide move constructor as well.
What is happening here? Let’s understand.
- Same as before, first
Creating personis coming from dynamically creatingPersoningetPersonfunction. - 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 ownershiplog. However, we saw in rvalue references that rvalue reference can only be created for a rvalue butpis not a rvalue. This is a special rule by the compiler where lvalue being returned can be moved instead of copy. - Now, in
mainfunction,getPersonreturns the temporary object which then move assigned top1.
Note
noexceptis 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:
- There is no user defined copy constructor and assignment operator.
- There is no user defined move constructor and assignment operator.
- 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);
}
// ...