Copy semantics defines how the copy of an object would be made. If an object has copy semantics, we can say that object is copyable. For class types, we have used copy constructors which are the implementations of copy semantics. When we initialize a class type object with another same type, the copy semantics get invoked (through copy constructor).
Copy semantic is implemented using copy constructor and overloading assignment operator for copy.
Copy constructor takes the other object of the same class type by reference and copies (shallow or deep) data member wise.
Overloading assignment operator also takes the same parameter, but it something more:
- Delete the old data (deallocate dynamic allocated memory for data members if any).
- Allocate new memory for dynamic data member if any and do copy.
- Finally, return the reference to object after copy.
An example
Following example shows a DynamicArray which wraps a c-style array. It implements copy semantic using copy constructor and copy assignment operator.
// program that demonstrates a custom dynamic array with copy semantic.
#include <iostream>
#include <cassert>
#include <algorithm>
class DynamicArray
{
int *m_arr{};
int m_length{};
void allocate(int size)
{
std::cout << "Allocating memory" << '\n';
m_arr = new int[size];
}
public:
DynamicArray() = default;
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=(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;
}
int &operator[](int index)
{
assert(index < m_length);
return m_arr[index];
}
int *begin() const
{
return m_arr;
}
int *end() const
{
return m_arr + m_length;
}
int length() const
{
return m_length;
}
};
void printArray(const DynamicArray &arr)
{
for (const int i : arr)
{
std::cout << i << '\n';
}
}
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 arr1;
arr1 = createArray();
return 0;
}Copy is not optimal
While copy semantics are useful, they are not optimal solution when it results in an expensive copy. For example, copying a DynamicArray of 100000 items to another array. In the above example of DynamicArray, I ran the program and got following output on my machine:
❯ ./DynamicArray.out
Allocating memory
Allocating memory
Deallocating memory
Allocating memory
Deallocating memory
Deallocating memory
Three copies are being made just to create a single array.
Note
You may get different output based on how you’ve compiled and which compiler version you are using.
If you use compiler prior to C++17, compiler performs mandatory copy optimization when an object is copy constructed using a temporary object of the same type. In this case, temporary object returned by
createArray().Furthermore, in
createArray()function we are returning the arrayarrwhich is used to create temporary object to be returned. Compiler can optimize this also where it elide the creation of temporary object and directly use to create the object in the caller. However, this is not mandatory and can be stopped using flag-fno-elide-constructorswhile compiling the program.Explained more in copy elision.
Let’s understand the flow:
- In
createArray, we are creating array using direct initialization constructor which dynamically allocates memory for the array. So, firstAllocating memoryis from here. - Next,
arrhas to be returned by value. Asarrwould be destroyed once the function finishes, a temporaryDynamicArrayobject gets copy created usingarr. The nextAllocating memorycomes from here. - Now, the function
createArrayis finishes and soarrwould be destroyed. This givesDeallocating memory. - Finally,
createArrayreturns temporary object which then gets used to copy assign toarr1. This calls copy assignment operator to copy temporary object toarr1. It gives the finalAllocating memorylog. - After the
createArrayexpression finishes, the temporary object also gets destroyed which givesDeallocating memorylog. - In the end when the
mainfinishes,arr1also gets destroyed and we canDeallocating memorylog.
Although some of the copy can be optimized by the compiler using copy elision but it still not an optimal choice. There is one more semantic for the type called move semantic which can help us here.