When we’re dealing with dynamic memory and smart pointers, interesting questions and problems soon begin to arise.
Consider the following setup. We have Character
objects who carry Sword
objects. When a character is created, it creates a sword for itself, and it dutifully cleans up the sword when it gets destroyed:
struct Sword {};
class Character {
public:
Character() : Weapon{new Sword{}} {}
~Character() { delete Weapon; }
Sword* Weapon;
};
However, problems will soon arise once we begin using this class. We’ll introduce some of the problems, and how to solve them throughout this lesson.
Let’s see what happens when we try to create a copy of instances of our Character
 class:
#include <iostream>
struct Sword {};
class Character {
public:
Character() : Weapon{new Sword{}} {};
~Character() { delete Weapon; }
Sword* Weapon;
};
int main() {
Character A{};
Character B{A};
std::cout << "A's Weapon: " << A.Weapon;
std::cout << "\nB's Weapon: " << B.Weapon;
}
The first problem is that our two characters are sharing the same Sword
, which is unlikely to be what we want:
A's Weapon: 000002894CBD6F90
B's Weapon: 000002894CBD6F90
This behavior is generally referred to as shallow copying. With shallow copying, only the properties on the "surface" are copied. So, we copy the pointer to the Sword
but below the surface, the sword itself wasn’t copied.
Another problem arises when one of our characters gets deleted - the shared weapon is also deleted by the destructor. Then, the other character has a pointer to an object that, unbeknownst to it, no longer exists. This is referred to as a dangling pointer, and it’s a problem that we generally try to avoid.
On some machines, this program will also crash right before it exits, suggesting a second problem. Specifically, both characters are trying to free the same memory address from their destructor.
That works fine when the first character is deleted, but when the second character is removed, the destructor will try to free a memory location that had already been freed. This is a double-free error, which will typically result in a crash.
Our previous example is problematic because it ignores a C++ convention called the rule of three. The rule states that if we define any of the following, we’ll probably want to define all of them:
Our previous example violates it because we’re defining a destructor, but not the other two. We introduce copy constructors and copy assignment operators later in this lesson.
We could try switching our implementation to use a shared pointer instead. This solves our crashing problem:
#include <iostream>
#include <memory>
struct Sword {};
class Character {
public:
Character()
: Weapon{std::make_shared<Sword>()} {};
std::shared_ptr<Sword> Weapon;
};
int main() {
Character A{};
Character B{A};
std::cout << "A's Weapon: " << A.Weapon;
std::cout << "\nB's Weapon: " << B.Weapon;
}
But, it doesn’t solve the copying problem - our characters are still sharing the same weapon:
A's Weapon: 000002E2E7C944E0
B's Weapon: 000002E2E7C944E0
In our lesson on memory ownership, we saw that unique pointers take steps to try to prevent themselves from being copied.
As such, when we try to implement our above code using unique pointers, the compiler will throw an error:
#include <iostream>
#include <memory>
struct Sword {};
class Character {
public:
Character()
: Weapon{std::make_unique<Sword>()} {};
std::unique_ptr<Sword> Weapon;
};
int main() {
Character A{};
Character B{A};
}
'Character::Character(const Character &)': attempting to reference a deleted function
This error is somewhat indirect - it does not mention unique pointers. Instead, it is telling us that it is trying to find a Character
constructor that takes a reference to another Character
and that the function has been deleted.
The first part of that error does make sense - the line in our code that is causing the error is Character B{A}
. This expression is indeed trying to construct a Character
by passing a reference to another Character
.
The constructor that handles code like this is referred to as the copy constructor. We haven’t defined one before, but code like this has generally worked anyway. This is because our classes come with default copy constructors.
However, now that we’ve added a unique pointer to our class, and unique pointers can’t be copied, the default copy constructor can no longer help us. That explains the second part of the error - the default copy constructor has been deleted.
So, we need to step in and define what it means to copy a Character
. We do this by defining the copy constructor ourselves, rather than relying on the default one.
The process of defining what it means for our objects to be copied is often referred to as implementing copy semantics.
In the previous example, we are constructing a Character
by passing a reference to an existing Character
. As with any construction, this calls a constructor on our class, specifically the copy constructor.
In the past, when we hadn’t defined a copy constructor, the compiler was calling a default one. That constructor was implementing the shallow copy behavior we were seeing.
Let's start to implement copy semantics, by implementing our copy constructor, thereby defining how our objects should be copied:
#include <iostream>
#include <memory>
struct Sword {};
class Character {
public:
Character()
: Weapon{std::make_unique<Sword>()} {};
Character(const Character& Source) {
std::cout << "I'm being copied!";
}
std::unique_ptr<Sword> Weapon;
};
int main() {
Character A{};
Character B{A};
std::cout << "\nA's Weapon: " << A.Weapon;
std::cout << "\nB's Weapon: " << B.Weapon;
}
Within our copy constructor, we’re constructing our new object, and we have a reference to our source object from which we can grab what we need. In the above constructor, we’re creating B
, and Source
is a reference to A
.
Typically we don’t want to modify the source object when copying it, so we mark that reference as const
.
Our code now compiles, although our new object doesn't have a Weapon
:
I'm being copied!
A's Weapon: 000001A6C70170D0
B's Weapon: 0000000000000000
Let's update our copy constructor so our new Character
gets their own Weapon
:
#include <iostream>
#include <memory>
struct Sword {};
class Character {
public:
Character()
: Weapon{std::make_unique<Sword>()} {};
Character(const Character& Source)
: Weapon{std::make_unique<Sword>()} {
std::cout << "I'm being copied!";
}
std::unique_ptr<Sword> Weapon;
};
int main() {
Character A{};
Character B{A};
std::cout << "\nA's Weapon: " << A.Weapon;
std::cout << "\nB's Weapon: " << B.Weapon;
}
Now, we’re successfully copying the Character
, and our new Character
gets its own Weapon
:
I'm being copied!
A's Weapon: 00000135B3CF6D90
B's Weapon: 00000135B3CF6E10
The previous example gives our new Character
its new Weapon
, but that might not be exactly what we want. Our second character's weapon isn’t copying the values from the original character’s weapon. Consider this example:
#include <iostream>
#include <memory>
struct Sword {
int Damage{10};
};
class Character {/*...*/};
int main() {
Character A{};
A.Weapon->Damage = 20;
Character B{A};
std::cout << "\nA's Weapon: " << A.Weapon;
std::cout << "\nA's Weapon Damage: "
<< A.Weapon->Damage;
std::cout << "\nB's Weapon: " << B.Weapon;
std::cout << "\nB's Weapon Damage: "
<< B.Weapon->Damage;
}
A's Weapon: 00000244F4126410
A's Weapon Damage: 20
B's Weapon: 00000244F4126FD0
B's Weapon Damage: 10
We modified A
's weapon damage to 20
before we created a copy, but the copy had the default value of 10
. This is because, within the Character
's copy constructor, we’re not passing any arguments when we create the Weapon
. Therefore, we’re creating the weapon using the default constructor:
Character(const Character& Source)
: Weapon{std::make_unique<Sword>()} {}
We may instead what to call the copy constructor of the Weapon
too, by passing in a reference to the source character’s weapon:
Character(const Character& Source)
: Weapon{std::make_unique<Sword>(
*Source.Weapon)} {}
The Weapon
class doesn’t use any unique pointers, so it still has its default copy constructor in place. Our code now looks like this:
#include <iostream>
#include <memory>
struct Sword { int Damage{10}; };
class Character {
public:
Character()
: Weapon{std::make_unique<Sword>()} {};
Character(const Character& Source)
: Weapon{std::make_unique<Sword>(
*Source.Weapon)} {}
std::unique_ptr<Sword> Weapon;
};
int main() {/*...*/}
A's Weapon: 0000021852C570D0
A's Weapon Damage: 20
B's Weapon: 0000021852C57110
B's Weapon Damage: 20
We’ve now deeply copied our Character
. Our new Character
's weapon has the same values as the source Character
's weapon. But, they are different weapons. Each Character
now has its own, which can subsequently be updated and deleted independently.
Copy construction isn’t the only way we can copy objects. We can also create copies with the assignment operator, =
. Let's make a small adjustment to our main
function to create an example of that:
#include <iostream>
#include <memory>
struct Sword { int Damage{10}; };
class Character {/*...*/};
int main() {
Character A{};
A.Weapon->Damage = 20;
Character B;
B = A;
}
Once again we’re getting a compiler error, which sounds similar to what we had before:
'&Character::operator =(const Character &)': attempting to reference a deleted function
Similar to the copy constructor, classes come with a copy assignment operator by default, which implements shallow copying. But, given our class contains a unique pointer that cannot be copied, we now have to extend our copy semantics to implement the copy assignment operator too.
The copy operator receives a reference to the object to be copied. Typically, we don’t want to modify the original object, so that reference will be const
.
Our function needs to return a reference to itself, which we can do via the this
 pointer.
Beyond these considerations, the implementation of copy ossignment perators tends to be very similar to copy constructors:
#include <memory>
struct Sword { int Damage{10}; };
class Character {
public:
Character()
: Weapon{std::make_unique<Sword>()} {};
// Copy constuctor
Character(const Character& Source)
: Weapon{std::make_unique<Sword>(
*Source.Weapon)} {}
// Copy assignment operator
Character& operator=(const Character& Source) {
Weapon =
std::make_unique<Sword>(*Source.Weapon);
return *this;
}
std::unique_ptr<Sword> Weapon;
};
If the above code is unclear, we have a two-part lesson on operator overloading which is likely to be helpful:
Our objects should now work as expected, regardless of how they are copied:
#include <iostream>
#include <memory>
struct Sword { int Damage{10}; };
class Character {/*...*/};
int main() {
Character A{};
A.Weapon->Damage = 20;
Character B;
B = A;
std::cout << "A's Weapon: " << A.Weapon;
std::cout << "\nA's Weapon Damage: "
<< A.Weapon->Damage;
std::cout << "\nB's Weapon: " << B.Weapon;
std::cout << "\nB's Weapon Damage: "
<< B.Weapon->Damage;
}
A's Weapon: 000001F5879668D0
A's Weapon Damage: 20
B's Weapon: 000001F587966F50
B's Weapon Damage: 20
There is an edge case where both operands to the copy operator can be the same object:
int main() {
Character A{};
A = A;
}
This is quite an unusual expression, but it is valid code, and it means we can’t assume that the argument passed to the copy operator is different from the object we’re updating. If the logic in our operator is predicated on that, we are leaving a landmine in our code.
We can test whether the objects are the same by comparing the address of the incoming object to the this
 pointer:
#include <iostream>
class Character {
public:
Character& operator=(
const Character& Source) {
if (this == &Source) {
std::cout << "Objects are the same\n";
} else {
std::cout << "Objects are different\n";
}
return *this;
}
};
int main() {
Character A{};
A = A;
Character B;
B = A;
}
Objects are the same
Objects are different
Copy semantics also come into play when we have code that implicitly creates copies of our object. Typically, this involves scenarios where we’re passing our objects around by value.
The main scenario where this happens is when we’re passing an object by value into a function parameter. There, the copy constructor is used to create the object that our function receives.
Below, we’ve defined a copy constructor for our class. We’re passing by value twice in the following code - once when we pass A
into our function parameter B
, and again when that function is returning B
The exact behavior of the following code can vary based on the compiler, but often, the copy constructor is used twice, and we have three different objects throughout the lifecycle of our program:
#include <iostream>
struct MyStruct {
MyStruct() = default;
MyStruct(const MyStruct& Source) {
std::cout << "\nCopying " << &Source
<< " to " << this;
}
};
MyStruct Function(MyStruct B) {
return B;
};
int main() {
MyStruct A{};
Function(A);
}
Copying 000000448D99FC34 to 000000448D99FD14
Copying 000000448D99FD14 to 000000448D99FD54
These two instructions requiring three different copies of our object in memory seem like a waste of resources.
In many cases, this is true - particularly if those objects are complex, and the act of deeply copying them is an elaborate process.
In the next lesson, we’ll see how we can implement move semantics, which allows us to make our classes more efficient.
Creating copies of our objects always has a performance overhead, so eliminating unnecessary copies is desirable. Compilers can help us here, through what is known as copy elision.
In the following example, our MyStruct
object is unlikely to be copied. In almost all compilers, our "Copying"
message will not be displayed:
#include <iostream>
struct MyStruct {
MyStruct() = default;
MyStruct(const MyStruct& Source) {
std::cout << "Copying\n";
}
};
MyStruct GetStruct() {
MyStruct Example{};
return Example;
};
int main() {
GetStruct();
std::cout << "Done";
}
Done
When the compiler sees our Example
struct being created in our GetStruct()
function, it understands where in memory our object is going to end up. From looking at the return value, Example
is guaranteed to end up in the stack frame of main()
. So it just creates it there directly.
As a result, when we reach the return
statement, we no longer need to copy our object from the GetStruct()
stack frame to the main()
stack frame - it was constructed in the main()
stack frame right from the start.
In this case, the compiler has avoided (or elided) the unnecessary copy. This is an example of return value optimization or RVO.
With a subtle change to our program, the compiler may no longer be able to invoke this form of RVO. Depending on the compiler, the following program likely creates both objects in the GetStruct()
frame and then copies the chosen one back to main()
.
This would trigger a single copy operation, noted by the output to the console:
#include <iostream>
struct MyStruct {
MyStruct() = default;
MyStruct(const MyStruct& Source) {
std::cout << "Copying\n";
}
};
MyStruct GetStruct(bool B) {
MyStruct Example1{};
MyStruct Example2{};
return B ? Example1 : Example2;
};
int main() {
GetStruct(true);
std::cout << "Done";
}
Copying
Done
The fact that a compiler can choose whether or not to call one of our functions is likely making some feel uneasy. The compiler making our program more performant is great, but by not calling the copy semantics we explicitly defined, it is also changing the behavior of our program in this case.
In this case, the compilers are doing what the C++ standard asks of them. The rules state that the compilers can (and in some cases, must) eliminate unnecessary copying, even if the copy constructor has side effects like updating global variables or outputting messages.
Because we don’t always know when copies are going to be created, it’s important that we only use the copy constructor for its specific purpose: copying objects.
If the compiler avoids creating a copy, any arbitrary code we had in a copy constructor will not run. But, if our constructor's only purpose is to copy objects, and no objects need to be copied, there’s no issue. We can enjoy the performance benefits the compiler gave us by not running it.
This same recommendation applies to destructors. When we have fewer copies, we also have fewer objects to destroy, so copy elision indirectly reduces the number of destructors being run.
In this lesson, we explored copy semantics. The key concepts we learned include:
Learn how to control exactly how our objects get copied, and take advantage of copy elision and return value optimization (RVO)
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.