Copy Constructors and Operators

Explore advanced techniques for managing object copying and resource allocation
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
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 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.

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

  • Should B.Weapon be the same object as A.Weapon? That is, should A.Weapon and B.Weapon point to the same memory address?
  • Should B.Weapon be a new object, initialized with the same state (Name, Damage, and Durability) that A.Weapon had when it was copied?
  • Should 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.

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

Copying Unique Pointers

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

Copy Constructors

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

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

Copy Assignment Operator

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

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

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

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.

Was this lesson useful?

Next Lesson

Managing Memory Manually

Learn the techniques and pitfalls of manual memory management in C++
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, Unlimited Access
Arrays and Dynamic Memory
3D art showing a progammer setting up a development environment
This lesson is part of the course:

Intro to C++ Programming

Become a software engineer with C++. Starting from the basics, we guide you step by step along the way

Free, unlimited access

This course includes:

  • 60 Lessons
  • Over 200 Quiz Questions
  • 95% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Managing Memory Manually

Learn the techniques and pitfalls of manual memory management in C++
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved