Welcome to the next pikoTutorial!
Nowadays there are many high level languages in which you don’t have to care about how objects are copied around. However, if you want to write in C++, you must understand that copying is a very distinct operation which varies depending on the object that you deal with. In theory, C++ also gets the job done for you because you can have completely empty class (no copy constructor defined) and you will still be able to copy such object:
class SomeClass {};
int main()
{
SomeClass a; // construction
SomeClass b(a); // copy construction
}
Note for beginners: if your class does not own any resource, consists only of trivially constructable types and does not encapsulate any logic, not implementing any custom constructors or desctructor is actually a good practice. This is called the rule of zero.
If so, then why would you ever bother writing your custom implementation of the copy constructor at all? Well, things start to get more complicated when your object are getting more complicated too.
Classes holding pointers
Imagine that your class allocates some memory and holds a pointer to that memory:
#include <iostream>
#include <memory>
class SomeClass
{
public:
SomeClass() : ptr_{std::make_shared<int>(0)} {}
void SetValue(const int value) { *ptr_ = value; }
int GetValue() const { return *ptr_; }
private:
std::shared_ptr<int> ptr_;
};
int main()
{
SomeClass a; // construction
SomeClass b(a); // copy construction
a.SetValue(24);
b.SetValue(36);
std::cout << a.GetValue() << std::endl; // one might expect 24, but the actual value is 36
std::cout << b.GetValue() << std::endl; // expected value: 36
}
The output of such code is:
36
36
Oops! It looks like with a call b.SetValue(36)
we have overwritten also the value in object a
. This is because the memory for ptr_
has been allocated only once – in the constructor of SomeClass
. It means, that ptr_
in copy b
still points to the same memory and by this, can change whatever resides in that memory.
Note for beginners: the thing that happened here is called a shallow copy.
To make a deep copy of the object, you must implement your custom copy constructor where you perform additional memory allocation dedicated for the copied object:
#include <iostream>
#include <memory>
class SomeClass
{
public:
SomeClass() : ptr_{std::make_shared<int>(0)} {}
SomeClass(const SomeClass& other) : ptr_{std::make_shared<int>(*other.ptr_)} {}
void SetValue(const int value) { *ptr_ = value; }
int GetValue() const { return *ptr_; }
private:
std::shared_ptr<int> ptr_;
};
int main()
{
SomeClass a; // construction
SomeClass b(a); // copy construction
a.SetValue(24);
b.SetValue(36);
std::cout << a.GetValue() << std::endl; // expected value: 24
std::cout << b.GetValue() << std::endl; // expected value: 36
}
Now the output is:
24
36
Note for beginners: when you create a class which manages certain resource (e.g. a pointer) and you need to implement a custom copy constructor, you most probably need to implement a move constructor as well and the assignment operators. This is called the rule of five.
Note for advanced: having
std::make_shared
only in a constructor allows you to control the dynamic memory allocation (e.g. by constructing all the objects at the startup of your application). However, putting it in the copy constructor introduces potentially unlimitted number of dynamic memory allocations during the application runtime. It leads to heap fragmentation which then may cause a heap/stack collision. This may be important in embedded systems with poor resources.
Classes holding non-copyable types
Another example may be a class which has a member variable whose copy constructor has been deleted:
#include <mutex>
class SomeClass
{
private:
std::mutex mtx_;
};
int main()
{
SomeClass a; // construction
SomeClass b(a); // copy construction
}
When we try to run such code, we will immediately get a compilation error:
main.cpp: In function ‘int main()’:
main.cpp:12:18: error: use of deleted function ‘SomeClass::SomeClass(const SomeClass&)’
12 | SomeClass b(a); // copy construction
| ^
What’s going on? Even if we know that std::mutex
has a deleted copy constructor, why the hell compiler says that our class has a deleted copy constructor? The reason for this is that whenever you put a non-copyable type inside your class and you don’t implement your custom copy constructor, the compiler implicitly deletes the constructor of your class as well. If you think about this, it makes sense bacause if by default it is not known what to do when someone tries to copy std::mutex
, it is up to you to decide about it inside your custom copy constructor. Otherwise, such operation is not allowed. Let’s then add our own copy constructor in which we just create a new mutex object:
#include <mutex>
class SomeClass
{
public:
SomeClass(const SomeClass&) : mtx_{} {}
private:
std::mutex mtx_;
};
int main()
{
SomeClass a; // construction
SomeClass b(a); // copy construction
}
Note for beginners: I didn’t want to obfuscate this copy constructor example with any additional logic, but if you encounter situation in which you need to copy class holding a mutex, don’t just blindly create a new mutex! Think about the behavior that you expect from the system and whether you really need to copy such class at all. In some cases, std::shared_mutex may come in handy.
When you try to run this code, you will notice that it’s not enough and you get a compilation error – this time the compiler doesn’t now how to construct our class on the first place:
main.cpp: In function ‘int main()’:
main.cpp:14:15: error: no matching function for call to ‘SomeClass::SomeClass()’
14 | SomeClass a; // construction
| ^
This is because adding a custom implementation of the copy constructor causes that the compiler stops generating the default constructor (and move constructor) for you class, so you must remember – whenever you add a copy constructor to your class, you must add a default constructor as well:
#include <mutex>
class SomeClass
{
public:
SomeClass() : mtx_{} {}
SomeClass(const SomeClass&) : mtx_{} {}
private:
std::mutex mtx_;
};
int main()
{
SomeClass a; // construction
SomeClass b(a); // copy construction
}
Now everything works without an error.
Classes caching values
The third example are classes which are supposed to persist a cache specific for a single class instance. Let’s look at this code:
#include <iostream>
class SomeClass
{
public:
SomeClass() : value_{0}, previous_value_{0} {}
void SetValue(const int new_value)
{
previous_value_ = value_;
value_ = new_value;
}
int GetPreviousValue() const { return previous_value_; }
private:
int value_;
int previous_value_;
};
int main()
{
SomeClass a; // construction
a.SetValue(12);
a.SetValue(24);
SomeClass b(a); // copy construction
std::cout << a.GetPreviousValue() << std::endl; // expected value: 12
std::cout << b.GetPreviousValue() << std::endl; // expected value: 0
}
We have here a simple class which holds some current value, but also a value which has been set previously. When we first set on the instance a
value 12 and 24 and then call a.GetPreviousValue()
, we expect it to return 12. The we construct b
by copying a
and we call b.GetPreviousValue()
. You could expect it to return a default value (in this case it’s 0) because no value has been set for this instance yet. However, the output shows us something different:
12
12
We see the value 12 which has been previously set on object a
, not on the object b
! The autogenerated default copy constructor has just copied everything what’s inside the class because it knows nothing about the logic that we expect from the entire code. Fix for this – adding a custom copy constructor in which you may want to copy the current value, but not the cached value:
#include <iostream>
class SomeClass
{
public:
SomeClass() : value_{0}, previous_value_{0} {}
SomeClass(const SomeClass& other) : value_{other.value_}, previous_value_{0} {}
void SetValue(const int new_value)
{
previous_value_ = value_;
value_ = new_value;
}
int GetPreviousValue() const { return previous_value_; }
private:
int value_;
int previous_value_;
};
int main()
{
SomeClass a; // construction
a.SetValue(12);
a.SetValue(24);
SomeClass b(a); // copy construction
std::cout << a.GetPreviousValue() << std::endl; // expected value: 12
std::cout << b.GetPreviousValue() << std::endl; // expected value: 0
}
Now the output is as expected:
12
0
Note for advanced: if you need to implement a caching-like mechanism, consider using
std::weak_ptr
for that purpose.