In this lesson, we’ll explore how objects are copied in more depth. There are two scenarios where our objects get copied. The first is when a new object is created by passing an existing object of the same type to the constructor:
struct Weapon{/*...*/};
int main() {
Weapon SwordA;
// Create SwordB by copying SwordA
Weapon SwordB{SwordA};
}
This copying process also happens when we pass an argument by value to a function. The function parameter is created by copying the object provided as the corresponding argument:
struct Weapon{/*...*/};
void SomeFunction(Weapon W) {/*...*/}
int main() {
Weapon Sword;
// Create the W parameter by copying Sword
SomeFunction(Sword);
}
The second scenario is when an existing object is provided as the right operand to the =
operator. In the following example, we’re expecting an existing PlayerTwo
to be updated by copying values from PlayerOne
:
struct Weapon{/*...*/};
int main() {
Weapon SwordA;
Weapon SwordB;
// Update SwordA by copying values from SwordB
SwordA = SwordB;
}
As we’ve likely noticed, C++ supports these behaviors by default, even when the objects we’re copying use our custom types. In this lesson, we’ll explore what that default behavior does, and learn how to override it when our classes and structs have more complex requirements.
The primary reason we need to override the default copying behavior is when our type is holding pointers to other types. These objects are often referred to as "resources" or "subresources". Below, our Player
type is holding a Sword
resource in a pointer called Weapon
:
struct Sword{
std::string Name{"Iron Sword"};
int Damage{42};
float Durability{1.0};
};
struct Player {
Player(Sword* Weapon) : Weapon{Weapon} {};
Sword* Weapon{nullptr};
};
When we copy a Player
object, we should consider how subresources, such as the Weapon
object, are handled as part of that process.
int main() {
Sword Weapon;
Player A{&Weapon};
A.Weapon->Durability = 0.9;
Player B{A};
B.Weapon; // What is this, exactly?
}
For example
B.Weapon
be the same object as A.Weapon
? That is, should A.Weapon
and B.Weapon
point to the same memory address?B.Weapon
be a new object, initialized with the same state (Name
, Damage
, and Durability
) that A.Weapon
had when it was copied?B.Weapon
be a new object, initialized with some default values, ignoring A.Weapon
's state?The correct answer to questions like this depends entirely on the type of object we’re creating and the requirements of our program. This lesson will explore how we can control this copying process for our custom types, thereby allowing us to implement the behaviors that are most appropriate for our use cases.
Let’s start by examining how C++ behaves by default when we copy objects. We have a Player
class that carries a Sword
, which it stores as a pointer:
struct Sword{};
struct Player {
Player(Sword* Weapon) : Weapon{Weapon} {};
Sword* Weapon{nullptr};
};
int main() {
Sword IronSword;
Player PlayerOne{&IronSword};
}
When we copy an object, we copy the pointers to its subresources, but not necessarily the objects to which they point. In the following example, we copy PlayerOne
to create PlayerTwo
.
Accordingly, PlayerOne.Weapon
is copied to create PlayerTwo.Weapon
. However, copying a pointer just means we now have two pointers pointing at the same underlying resource:
#include <iostream>
struct Sword{};
struct Player {
Player(Sword* Weapon) : Weapon{Weapon} {};
Sword* Weapon{nullptr};
};
int main() {
Sword IronSword;
Player PlayerOne{&IronSword};
Player PlayerTwo{PlayerOne};
if (PlayerOne.Weapon == PlayerTwo.Weapon) {
std::cout << "Players sharing same weapon";
}
}
Players sharing same weapon
We can imagine this situation as representing PlayerOne
and PlayerTwo
sharing the exact same weapon. This is unlikely to be what we want and could cause problems as we build out our program with more complex behaviors.
For example, if PlayerOne
modifies the weapon in some way, those changes will affect PlayerTwo
too. As we begin to rely more heavily on manual memory management, this will cause further resource management problems which we’ll discuss in the next lesson.
We covered std::unique_ptr
in the previous lesson, and in particular, we introduced how unique pointers prevent this sharing problem by preventing themselves from being copied:
#include <memory>
struct Sword {};
int main() {
auto WeaponA{std::make_unique<Sword>()};
std::unique_ptr<Sword> WeaponB{WeaponA};
}
error C2280: 'std::unique_ptr<Sword>(const std::unique_ptr<Sword>&)': attempting to reference a deleted function
note: 'std::unique_ptr<Sword>(const std::unique_ptr<Sword>&): function was explicitly deleted
This also applies to classes that have std::unique_ptr
member variables. If a class has such a variable, the objects of that class cannot be copied by default, as they contain a member variable that cannot be copied.
Let’s update our Player
class to store its Weapon
as a unique pointer (std::unique_ptr<Sword>
) rather than a raw pointer (Sword
), so we can see this in action:
#include <iostream>
#include <memory>
struct Sword{};
struct Player {
Player()
: Weapon{std::make_unique<Sword>()} {}
std::unique_ptr<Sword> Weapon;
};
int main() {
Player PlayerOne;
Player PlayerTwo{PlayerOne};
}
error: 'Player::Player(const Player&)': attempting to reference a deleted function
note: 'Player::Player(const Player&)': function was implicitly deleted because a data member invokes a deleted or inaccessible function 'std::unique_ptr<Sword>(const std::unique_ptr<Sword>)'
note: 'std::unique_ptr<Sword>(const std::unique_ptr<Sword>)': function was explicitly deleted
If we pay close attention to the previous error message, we can note that the line that is attempting to copy PlayerOne
is calling the function Player::Player(const Player&)
We might also recognize that the Player::Player
syntax identifies this function as a Player
constructor. We can also see it seems to be receiving a const Player&
as an argument - that is, a constant reference to another Player
object.
This constructor is called the copy constructor. It is invoked when we construct a new object from an existing object of the same type.
As we’ve seen in the past, we can copy objects by default. This is because the compiler provides default copy constructors for the types we create. We can replace these default constructors with our own custom implementations.
Let’s see an example by adding a custom copy constructor to our Sword
:
struct Sword {
Sword(const Sword& Original) {
std::cout << "Copying Sword\n";
}
};
As usual, defining a constructor will delete the default constructor, but we can re-add it if needed:
struct Sword {
Sword() = default;
Sword(const Sword& Original) {
std::cout << "Copying Sword\n";
}
};
As we might expect, the copy constructor is invoked any time we construct a new object using an existing object of the same type. This includes when a function needs to create an object based on an argument passed to it by value:
#include <iostream>
struct Sword {
Sword() = default;
Sword(const Sword& Original) {
std::cout << "Copying Sword\n";
}
};
void SomeFunction(Sword) {}
int main() {
Sword WeaponA;
// Constructing new Swords by copying WeaponA
Sword WeaponB{WeaponA};
Sword WeaponC = WeaponA;
// Passing by value is copying, too
SomeFunction(WeaponA);
}
Copying Sword
Copying Sword
Copying Sword
Let’s return to our original example, and implement the copy constructor for our Player
objects. We’ll simply replicate the behavior of the default copy constructor, with some additional logging:
#include <iostream>
struct Sword {
Sword() = default;
Sword(const Sword& Original) {
std::cout << "Copying Sword\n";
}
};
struct Player {
Player(Sword* Weapon) : Weapon{Weapon} {}
Player(const Player& Original) {
Weapon = Original.Weapon;
std::cout << "Copying Player\n";
}
Sword* Weapon{nullptr};
};
int main() {
Sword IronSword;
Player PlayerOne{&IronSword};
Player PlayerTwo{PlayerOne};
if (PlayerOne.Weapon == PlayerTwo.Weapon) {
std::cout << "Players sharing same weapon";
}
}
As we can see from the output, the copy constructor for Sword
is not invoked, and both of our Player
objects are left sharing the same weapon:
Copying Player
Players sharing same weapon
As with any constructor, we can use a member initializer list with our copy constructor. This is the preferred way to set the initial values of member variables, so we should use it where possible:
struct Player {
Player(Sword* Weapon) : Weapon{Weapon} {}
Player(const Player& Original)
: Weapon{Original.Weapon} {
std::cout << "Copying Player\n";
}
Sword* Weapon{nullptr};
};
From a member initializer list, we can also invoke any other constructor. We already have a constructor that initializes the Weapon
variable, so we can call that from the initializer list of our copy constructor:
struct Player {
Player(Sword* Weapon) : Weapon{Weapon} {}
Player(const Player& Original)
: Player{Original.Weapon} {
std::cout << "Copying Player\n";
}
Sword* Weapon{nullptr};
};
Let’s fix our root problem. Instead of having our copies share their subresources, let’s ensure every copy gets a complete copy of the subresources too.
This is easier and safer to do using smart pointers, so we’ll switch our implementation back to using a std::unique_ptr
for now. We’ll cover how to do this using raw pointers in the next lesson.
Remember, the function arguments passed to std::make_unique()
are forwarded to the constructor of the type we’re creating. We can invoke the copy constructor of that type by passing the object we want to copy.
This may include dereferencing a pointer to that object if necessary:
auto WeaponA{std::make_unique<Sword>()};
auto WeaponB{std::make_unique<Sword>(*WeaponA)};
Let’s use this in our Player
example, where we retrieve the Weapon
we want to copy by dereferencing the original player’s Weapon
:
#include <iostream>
struct Sword {
Sword() = default;
Sword(const Sword& Original) {
std::cout << "Copying Sword\n";
}
};
struct Player {
Player() : Weapon{std::make_unique<Sword>()} {}
Player(const Player& Original)
: Weapon{std::make_unique<Sword>(
*Original.Weapon
)} {
std::cout << "Copying Player\n";
}
std::unique_ptr<Sword> Weapon;
};
int main() {
Player PlayerOne;
Player PlayerTwo{PlayerOne};
if (PlayerOne.Weapon != PlayerTwo.Weapon) {
std::cout << "Players are NOT sharing "
"the same weapon";
}
}
As we can see from the output, the entire Sword
object is now being copied, rather than just a pointer to it. As a result, our Player
objects are no longer sharing the same Sword
- they each get their own.
Copying Sword
Copying Player
Players are NOT sharing the same weapon
Fully copying an object in this way is often referred to as deep copying. Before, where we were simply copying the pointer to the same underlying object, is referred to as shallow copying.
There is another context in which objects are copied that we need to be mindful of when creating our classes. This happens when the =
operator is used to update an existing object using the values of some other object of the same type:
Player PlayerOne;
Player PlayerTwo;
PlayerTwo = PlayerOne;
As we’ve seen in the past, the compiler provides a default implementation of this operator for our types. The default implementation of this operator behaves in the same way as the default copy constructor, shallow-copying values from the right operand to the left operand.
In this case, our type contains a std::unique_ptr
, which cannot be copied. As such, we get a similar error to what we had before if we attempt to use the =
operator:
#include <iostream>
struct Sword {/*...*/};
error C2280: 'Player& Player::operator =(const Player&)': attempting to reference a deleted function
note: 'Player& Player::operator=(const Player&)': function was implicitly deleted because a data member invokes a deleted or inaccessible function
note: 'std::unique_ptr<Sword>::operator=(const std::unique_ptr<Sword>&)': function was explicitly deleted
We can provide a custom implementation of this operator, using the same syntax we would when overloading any other operator. The error message above hints at what the function signature would be:
Player& Player::operator=(const Player&)
That is, a function called operator=
in the Player
class that accepts a const reference to a Player
object and returns a Player
reference. The basic scaffolding would look like this:
#include <iostream>
struct Sword {/*...*/};
Our program will now compile, but our copy assignment operator isn’t doing anything useful. Let’s make it copy the Weapon
.
However, unlike with the copy constructor, the copy assignment operator is not creating a new Player
object. Rather, it is updating an existing Player
, and this existing Player
already has a Weapon
.
Depending on our specific requirements, there are two main options we have to deal with this. We can update the existing Weapon
to match the values from the Weapon
we want to copy. That typically involves using the =
operator, thereby calling the copy assignment operator on the Sword
class:
#include <iostream>
struct Sword {/*...*/};
Copying Sword by Assignment
Alternatively, we can delete the existing Weapon
and construct a new one, thereby calling the copy constructor for the Sword
type.
As we saw within the Player
copy constructor, we can pass the original player’s Weapon
as the argument to std::make_unique<Sword>()
:
std::make_unique<Sword>(*Original.Weapon)
This will invoke the Sword
type’s copy constructor and return a std::unique_ptr
that manages this new Sword
.
The std::unique_ptr
type has overloaded the =
operator make updates like this easier. It ensures the object it was previously managing is deleted, so updating a std::unique_ptr
looks much the same as updating any other type:
#include <iostream>
struct Sword {/*...*/};
Copying Sword by Constructor
When defining a copy assignment operator, there is an edge case we need to consider: that both operands are the same object. This is rare, but technically valid code:
int main() {
Player PlayerOne;
PlayerOne = PlayerOne;
}
We should ensure the logic in our operator remains valid in this scenario.
The most common approach is to have our operator start by comparing the this
pointer to the memory address of the right operand. If these values are equal, both of our operands are the same.
When this is the case, our copy operator typically doesn’t need to do anything, so we can return
immediately:
struct Player {
Player() {/*...*/};
Player(const Player& Original) {/*...*/};
// Copy assignment operator
Player& operator=(const Player& Original) {
if (&Original == this) {
return *this;
}
Weapon = std::make_unique<Sword>(*Original.Weapon);
return *this;
}
std::unique_ptr<Sword> Weapon;
};
As we’ve seen before, if a type doesn’t implement a copy constructor and copy assignment operator, the compiler generates them by default.
These default implementations simply iterate through all the member variables and call the copy constructor or copy assignment operator associated with their respective types. This process respects any custom implementations we’ve provided for those types.
For example, below we define a Party
struct that contains Player
objects. The Party
class uses the default copy functions, whilst Player
has provided custom implementations.
When we copy Party
objects, the custom Player
copy constructor and assignment operators are automatically invoked when appropriate:
#include <iostream>
struct Player {
Player() = default;
Player(const Player& Original) {
std::cout << "Copying Player by Constructor\n";
}
Player& operator=(const Player& Original) {
std::cout << "Copying Player by Assignment\n";
return *this;
}
};
struct Party {
Player PlayerOne;
// Other Players
// ...
};
int main() {
Party PartyOne;
Party PartyTwo{PartyOne};
PartyOne = PartyTwo;
}
Copying Player by Constructor
Copying Player by Assignment
This means that, in general, objects that store subresources do not need to intervene to control how those subresources are copied, even if those subresources have non-standard requirements.
We simply implement those requirements by defining copy constructors and operators on the type that needs them. Objects that store instances of those types will then apply those behaviors automatically, with no additional effort required.
Sometimes, we don’t want our types to support copying. This can be an intentional design choice like with std::unique_ptr
, or perhaps supporting copying for our type would be a lot of work, and we’d rather not do it until we’re sure we need that capability.
In either case, we should explicitly delete the default copy constructor and copy assignment operator. The syntax for deleting constructors and operators looks like this:
struct Player {
Player() = default;
Player(const Player&) = delete;
Player& operator=(const Player&) = delete;
};
Now, if someone tries to copy our object, they’ll get a compiler error rather than a program that could have memory issues or other bugs:
struct Player {/*...*/};
int main() {
Player PlayerOne;
Player PlayerTwo{PlayerOne};
Player PlayerThree;
PlayerThree = PlayerOne;
}
error C2280: 'Player::Player(const Player&)': attempting to reference a deleted function
note: 'Player::Player(const Player&)': function was explicitly deleted
error C2280: 'Player& Player::operator=(const Player&)': attempting to reference a deleted function
note: 'Player& Player::operator=(const Player&)': function was explicitly deleted
std::shared_ptr
This lesson focused on scenarios where objects want unique ownership over their subobjects, but that is not always the case. In some scenarios, it’s more appropriate for ownership of some resource to be shared among multiple objects.
The standard library provides another smart pointer for this scenario: std::shared_ptr
. Whilst a std::unique_ptr
automatically releases the resource it’s managing once its unique owner is destroyed, a std::shared_ptr
will release its object only when all of its shared owners are deleted.
To facilitate shared ownership, shared pointers can naturally be copied, and they provide utilities such as a use_count()
method to return how many owners the pointer currently has:
#include <iostream>
#include <memory>
struct Quest {};
struct Player {
Player()
: CurrentQuest{std::make_shared<Quest>()} {}
std::shared_ptr<Quest> CurrentQuest;
};
int main() {
Player One;
std::cout << "Quest Owner Count: "
<< One.CurrentQuest.use_count();
// Create a copy
Player Two{One};
if (One.CurrentQuest == Two.CurrentQuest) {
std::cout << "\nPlayers have same quest";
}
std::cout << "\nQuest Owner Count: "
<< One.CurrentQuest.use_count();
}
Quest Owner Count: 1
Players have same quest
Quest Owner Count: 2
We cover std::shared_ptr
in more detail in a dedicated lesson in the advanced course.
In this lesson, we've explored the intricacies of object copying in C++. We've covered copy constructors, copy assignment operators, and how to implement custom copying behavior. You've learned about shallow vs. deep copying, resource management, and how to prevent copying when necessary.
With this knowledge, you're now equipped to fully control object duplication in your programs. We’ll build on these techniques and learn how to manually manage memory in the next lesson.
Explore advanced techniques for managing object copying and resource allocation
Become a software engineer with C++. Starting from the basics, we guide you step by step along the way