Copy Constructors and Operators

Explore advanced techniques for managing object copying and resource allocation

Ryan McCombe
Updated

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 SwordA to be updated by copying values from SwordB:

struct Weapon{/*...*/};

int main() {
  Weapon SwordA;
  Weapon SwordB;
  
  // Update SwordA by copying values from SwordB
  SwordA = SwordB;
}

As we've noticed, C++ supports these behaviors by default, even when the objects we're copying use our custom types.

However, sometimes, those default implementations don't work exactly how we want. When we create a custom type, such as our Weapon example above, we sometimes need to control how instances of that type should be copied.

In this lesson, we'll explain why this is sometimes necessary, and learn how to take control over the copying process.

Subresources

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" of our primary object.

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

  1. Should B.Weapon be the same object as A.Weapon? That is, there is only a single weapon which both player A and B are sharing, meaning that A.Weapon and B.Weapon are both pointing to the same memory address.
  2. Should B.Weapon be a new object, initialized with the same state (Name, Damage, and Durability) that A.Weapon had when it was copied?
  3. Should B.Weapon be a new object, initialized with some default values, ignoring A.Weapon's state?

The default copying behavior assumes we want option 1, where both copies will then share the same resource. We'll explore what that actually means in the next section.

If we want our Player objects to copy their Weapon using option 2 or 3 instead, or something else entirely, then we need to intervene and provide that custom copying logic. This will be the focus of the rest of the lesson.

Sharing Resources

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 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 does not copy the object it was pointing at - it just means we now have two pointers pointing at the same underlying object. We can confirm this by checking that the PlayerOne.Weapon and PlayerTwo.Weapon pointers are equal:

#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 somehow wielding the exact same weapon. This is unlikely to be what we want for our game simulation, and could cause problems as we build out our program with more complex behaviors.

For example, if PlayerOne modifies their weapon in some way, those changes will affect PlayerTwo too. As we begin to rely more heavily on manual memory management, this could cause further resource management problems which we'll discuss in the next lesson.

Copy Constructors

To intervene and customize how objects of our type get copied, we need to define two particular functions on that type's class or struct:

  • The copy constructor
  • The copy assignment operator

As we've seen in the past, we can copy objects by default, even though we haven't been defining these functions. This is because the compiler provides default versions of these functions for the types we create. We can replace these default functions with our own custom implementations if we need to.

Let's see an example of adding a custom copy constructor to our Sword struct. Like any constructor, the name of the function matches the name of the class or struct. In the case of the copy constructor, the argument will be a constant reference to another object of that same type.

We'll call that other object Original, in this example:

struct Sword {
  Sword(const Sword& Original) {
    std::cout << "Copying Sword\n";
  }
};

As usual, defining any constructor will delete the default constructor. If we want our type to continue to be constructible without any arguments, we now need to explicitly define that constructor.

If we want it to use the compiler-provided default implementation as before, we can use the = default syntax:

struct Sword {
  Sword() = default;
  Sword(const Sword& Original) {
    std::cout << "Copying Sword\n";
  }
};

Using the Copy Constructor

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}; // Copy 1
  Sword WeaponC = WeaponA; // Copy 2

  // Passing by value is copying, too
  SomeFunction(WeaponA); // Copy 3
}
Copying Sword
Copying Sword
Copying Sword

Copying Player Objects

Let's return to our original example, and reexamine the problem of Player copies sharing the same Weapon. To start to solve this, we'll need to implement a custom copy constructor for our Player objects.

For now, we'll simply replicate the behavior of the default copy constructor where we copy the pointer to the Weapon, but not the Weapon itself. We'll also add some 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

We'll finally fix this in the next section, but let's quickly review in this context.

Member Initializer Lists

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};
};

Deep Copying

Let's finally fix our root problem. Instead of having our copies share their subresources, we'll 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 to use 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. In the following example, we default-construct WeaponA, and then copy-construct WeaponB by using WeaponA as the original:

auto WeaponA{std::make_unique<Sword>()};
auto WeaponB{std::make_unique<Sword>(*WeaponA)};

Let's use this technique 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";
  }
}

If we compile and run our program now, the output should confirm that the entire Sword object is 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

Copy Assignment Operator

There is another context in which objects are copied that we need to be mindful of when creating our types. This happens when the = operator is used to update an existing object using the values of some other object of the same type.

This is called the copy assignment operation:

Player PlayerOne;
Player PlayerTwo;

// Use the copy assignment operator
PlayerTwo = PlayerOne;

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 has been updated to use a std::unique_ptr, which cannot be copied by default. As such, we'll get a compilation error if we try to use the = operator in this way:

#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.

Therefore, the basic scaffolding for overloading this operator would look like this:

#include <iostream>

struct Sword {/*...*/}; struct Player {
Player() {/*...*/};
Player(const Player& Original) {/*...*/}; // Copy assignment operator Player& operator=(const Player& Original) { // Update this Player using data from Original // ... // Then return a reference to it: return *this; } std::unique_ptr<Sword> Weapon; }
int main() {/*...*/};

Our program will now compile, but our copy assignment operator isn't doing anything useful. Let's make it copy the Sword.

However, unlike with the copy constructor, the copy assignment operator isn't creating a new Player object. Rather, it is updating an existing Player, and this existing Player already has a Sword, which it stores as a std::unique_ptr called Weapon.

Depending on our specific requirements, there are two main options we have to deal with this.

Option 1: Update the Existing Sword

The first approach is that we can update the existing Sword to match the values from the Sword we want to copy.

That typically involves dereferencing the Weapon pointer to access the underlying Sword, and then using the = operator on that Sword:

*Weapon = *Original.Weapon;

This calls the copy assignment operator on the underlying Sword type, passing the Sword held by the original character's Weapon pointer.

In this example, we've overloaded the copy assignment operator on Sword too, just to add some logging and confirm it's being used:

#include <iostream>

struct Sword {
  Sword() = default;
  Sword(const Sword& Original) {
    std::cout << "Copying Sword by Constructor\n";
  }
  Sword& operator=(const Sword& Original) {
    std::cout << "Copying Sword by Assignment\n";
    return *this;
  }
};

struct Player {
Player() {/*...*/};
Player(const Player& Original) {/*...*/}; // Copy assignment operator Player& operator=(const Player& Original) { *Weapon = *Original.Weapon; return *this; } std::unique_ptr<Sword> Weapon; };
Copying Sword by Assignment

Option 2: Replace the Existing Sword

Alternatively, we can delete the existing Sword and construct a new one using the Sword type's copy constructor.

As we saw within the Player copy constructor, we can pass the original player's Sword as the argument to std::make_unique<Sword>(). We can access their Sword by dereferencing the Weapon pointer that manages it:

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 to 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 {
  Sword() = default;
  Sword(const Sword& Original) {
    std::cout << "Copying Sword by Constructor\n";
  }
  Sword& operator=(const Sword& Original) {
    std::cout << "Copying Sword by Assignment\n";
    return *this;
  }
};

struct Player {
Player() {/*...*/};
Player(const Player& Original) {/*...*/}; // Copy assignment operator Player& operator=(const Player& Original) { Weapon = std::make_unique<Sword>( *Original.Weapon); return *this; } std::unique_ptr<Sword> Weapon; };
Copying Sword by Constructor

Note that the left operand of the = operator is different between these two options. In the first option, the left operand was *Weapon, whilst in option two, it is Weapon.

Weapon is the std::unique_ptr, whilst *Weapon dereferences that pointer to access the underlying Sword:

// Option 1: Call the = operator on `Sword`
*Weapon = *Original.Weapon

// Option 2: Call the copy constructor on Sword,
// followed by the = operator on `std::unique_ptr`
Weapon = std::make_unique<Sword>(*Original.Weapon)

Copying an Object to Itself

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; };

Recursive Copying

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 does not define a copy constructor or assignment operator, so it just uses the default implementations provided by the compiler.

However, when we copy Party objects, these default functions still respect the custom Player copy constructor and assignment operators that we defined:

#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.

Preventing Copying

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

Preview: Sharing Ownership using 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.

For example, perhaps both of our players can share the same quest, with contributions from either player being tracked by the same underlying object.

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 is being shared by "
    << One.CurrentQuest.use_count() << " player";

  // Create a copy
  Player Two{One};
  
  if (One.CurrentQuest == Two.CurrentQuest) {
    std::cout << "\nBoth players have the same quest";
  }
  std::cout << "\nQuest is being shared by "
    << One.CurrentQuest.use_count() << " players";
}
Quest is being shared by 1 player
Both players have the same quest
Quest is being shared by 2 players

We cover std::shared_ptr in more detail in a dedicated lesson in the advanced course.

Summary

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.

Next Lesson
Lesson 48 of 61

Managing Memory Manually

Learn the techniques and pitfalls of manual memory management in C++

Questions & Answers

Answers are generated by AI models and may not have been reviewed. Be mindful when running any code on your device.

Ensuring Consistency in Copy Operations
How can I ensure that my custom copy constructor and assignment operator are consistent with each other?
Returning References in Copy Assignment
Why do we return a reference to the object in the copy assignment operator?
Copy Operations and Inheritance
How do copy constructors and assignment operators interact with inheritance?
Explicit vs Implicit Copy Operations
What's the difference between explicit and implicit copy operations?
Performance: Deep vs Shallow Copying
What are the performance implications of deep copying versus shallow copying?
Or Ask your Own Question
Get an immediate answer to your specific question using our AI assistant