This is the second part regarding rvalue reference and move semantic. In this part we will try to make improvements to some of our created constructors. Let's check our assignment operator.
As a quick reminder, the copy assignment operator is called when we replace an existing object.
Object& operator=(const Object& other)
{
if(this == &other) return *this;
delete[] data;
data = new int[other.size]; // (1)
std::copy(other.data, other.data + other.size, data);
size = other.size;
return *this;
}
This seems maybe okay at first glance, but there are some serious issues. What if new fails? What if it throws something (1)? Then we have deleted our content in data and left it in a invalid state. Additionally the size variable of *this is also wrong probably.
Object& operator=(const Object& other)
{
if( this == &other ) return *this;
std::size_t tmpSize = other.size; // (1)
int32_t* tmpArray = tmpSize ? new int32_t[ tmpSize ]() : nullptr; // (2)
std::copy(other.data, other.data + tmpSize, tmpArray); // (3)
delete[] data; // (4)
size = tmpSize; // (4)
data = tmpArray; // (4)
return *this;
}
Now we have restructured the code a bit. First we create a temporary array (2) based on the size of other.
The call new int32_t[ tmpSize ]()
Creates a new array on the heap and the two parantheses inizialize all values to 0 of the array.
After that we copy the data from the other array to our tmpArray (3). Now we have securely avoided probles if an exception is thrown.
The lines at (4) do not throw exceptions and we only switch the pointer of the data field of *this and the size of *this. Now we can sleep a bit better.
Our new version of the assignment operator is good but there is quite some code in it which we also have in other constructors, this is not ideal. It is important to learn that, as soon as your class works with some kind of resources it is a good idea to also implement a swap function.
class Object
{
public:
// ...
friend void swap(Object& first, Object& second) // nothrow
{
use std::swap;
swap(first.size, second.size);
swap(first.data, second.data);
}
// ...
};
Our swap function is implemented it only uses std::swap on each of the fields of the object. Check resources to see why we use friend. Now lets check the assignment operator again.
Object& operator=(Object other) // (1)
{
swap(*this, other); // (2)
return *this;
}
The very intersting part is in the signature of the function, we see that we no longer take the parameter by reference but instead we take it by value. As we know if we take a parameter by value then the compiler creates a copy of the variable for us already and then we no longer have to create an copy of the variable ourself in the function. Basically the copy constructor is now called in (1) for the other object. The advantage is, that on enter of the function we already have an copy of the other object which we know allocated everything correctly because if the copy constructor would fail, we would not enter the function. Before we were forced to take care of the situation when a exception is thrown. The copy of the other object will be cleaned up after we exit the scope.
We were able to drastically minimize the lines of code in our assignment operator. And the compiler guarantees that we are exceptions safe.
As a reminder our current move constructor looks like this:
Object(Object&& other)
{
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
A better way of implementing this with our new swap ability is:
class Object
{
//....
public:
Object(Object&& other) noexcept
: Object() // (1)
{
swap(*this, other);
}
}
So what I was interested was the line (1) and why we call the default constructor. This is called constructor delegation, in the initializer list we can call other constructors of this class to handle intializations for us. This is a way to follow the DRY principle. In this case it makes sure that other will be set to a valid but unspecified state. Now we have also decreased the lines of code for the move constructor and also we can set the constructor to noexcept. The default constructor should not fail.