copy-and-swap idiom solves two main problems with copy-semantic using copy constructor and copy assignment operator that we have seen in note on overloading assignment operator.
For recap, let’s see the original code:
class DynamicArray
{
//...
// copy constructor
DynamicArray(const DynamicArray &other)
: m_length{other.m_length}
{
allocate(m_length);
std::copy_n(other.m_arr, m_length, m_arr);
}
// copy assignment operator
DynamicArray &operator=(const DynamicArray &other)
{
if (this == &other)
return *this;
// deleting old stuff
delete[] m_arr;
m_arr = nullptr;
m_length = 0;
// new stuff
allocate(other.m_length);
m_length = other.m_length;
std::copy_n(other.m_arr, m_length, m_arr);
return *this;
}
//...
};So, the problems with this approach are:
- Although the conditional for self-assignment serves a useful purpose but it also makes the code noisy and slow. Also, it would be a rare case when a self-assignment occurs. It would be better if something other approach could be taken here without this conditional.
- Second issue, we are allocating new resources here which may fail (if memory is not enough). This exception has to be handled properly. The same thing we would need to do in copy constructor. Doing it in both places would introduce code duplication that we want to avoid.
copy-and-swap
In copy-and-swap, assignment operator should already get a copy of the object (previously it was happening inside the function) and then it should swap the current object data with data of copy object.
Copy
We can get the copy by changing the parameter type from reference to value. This way, when object to be copied is passed by value whose copy is created using copy constructor we have defined. So, this becomes the responsibility of copy constructor to properly create the copy and handle exceptions if any. Compiler is doing the job of calling that copy constructor when passed by value.
class DynamicArray
{
//...
// copy constructor
DynamicArray(const DynamicArray &other)
: m_length{other.m_length}
{
allocate(m_length);
std::copy_n(other.m_arr, m_length, m_arr);
}
// copy assignment operator
DynamicArray &operator=(DynamicArray other)
{
//...
}
//...
};Swap
Second we need a swap function which swaps two objects of type DynamicArrays by swapping member wise.
class DynamicArray
{
// ... other
friend void swap(DynamicArray &lhs, DynamicArray &rhs)
{
using std::swap;
swap(lhs.m_arr, rhs.m_arr);
swap(lhs.m_length, rhs.m_length);
}
// ...other
}Finally, our operator looks like:
class DynamicArray
{
//...
// copy assignment operator
DynamicArray &operator=(DynamicArray other)
{
swap(*this, other);
return *this;
}
//...
};That’s all.
Final
Final code looks like:
#include <iostream>
#include <cassert>
#include <algorithm>
class DynamicArray
{
int *m_arr{nullptr};
int m_length{};
void allocate(int size)
{
std::cout << "Allocating memory" << '\n';
m_arr = new int[size];
}
public:
DynamicArray()
{
// just doing to show allocation.
allocate(0);
}
DynamicArray(int length)
: m_length{length}
{
allocate(length);
}
~DynamicArray()
{
std::cout << "Deallocating memory" << '\n';
delete[] m_arr;
}
// copy constructor
DynamicArray(const DynamicArray &other)
: m_length{other.m_length}
{
allocate(m_length);
std::copy_n(other.m_arr, m_length, m_arr);
}
// copy assignment operator
DynamicArray &operator=(DynamicArray other)
{
std::cout << "Copy assignment" << '\n';
swap(*this, other);
return *this;
}
friend void swap(DynamicArray &lhs, DynamicArray &rhs)
{
using std::swap;
swap(lhs.m_arr, rhs.m_arr);
swap(lhs.m_length, rhs.m_length);
}
int &operator[](int index)
{
assert(index < m_length);
return m_arr[index];
}
int length() const
{
return m_length;
}
};
DynamicArray createArray()
{
DynamicArray arr(100000);
for (int i{0}; i < arr.length(); ++i)
{
arr[i] = i;
}
return arr;
}
int main(int argc, char const *argv[])
{
DynamicArray arr(1);
arr = createArray();
return 0;
}Why it works?
- In the assignment operator, we are already getting the copy of the object where compiler creates the copy using copy constructor.
- We got the copy, we need to swap the current object data with the copy object. That we do using our friend function
swap(why a separate friend function, why directly notstd::swap? I’ll talk about it later in this note). - After the swap happens in the
swapfriend function, we return the updated object. When the assignment operator function finishes, the copy object with swapped data, gets destroyed.
Why swap as friend function?
Our DynamicArray is swappable and it is a better practice for a swappable object to have its own swap function. Making swap as friend function allows to overload swap() calls where a better match to swap can be found using ADL.
For example:
using std::swap;
swap(dynamicArray1, dynamicArray2);So, what happens here, if dynamicArray1 or dynamicArray2 has swap function, C++ would use that function (because of ADL would make it accessible), otherwise call to std::swap would happen.
One more thing, we can’t do std::swap(*this, other) because std::swap uses copy (or move) constructor and assignment operator function of the arguments which would incur infinite recursion.