Copy Semantics and Return Value Optimization

Learn how to control exactly how our objects get copied, and take advantage of copy elision and return value optimization (RVO)
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Updated

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.

Why do we need Copy Semantics?

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

Shallow Copying

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.

The Rule of Three

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:

  • a destructor
  • a copy constructor
  • a copy assignment operator

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.

Using Shared Pointers

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

Using Unique Pointers

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.

Implementing Copy Semantics

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

Deep Copying

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 Assignment Operator

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

Copying Objects to Themselves

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 and Pass-by-Value

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.

Copy Elision and Return Value Optimization

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

Copy Semantics and Side Effects

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.

Summary

In this lesson, we explored copy semantics. The key concepts we learned include:

  • The difference between shallow copying and deep copying.
  • The Rule of Three in C++, which suggests that if you implement any of the destructor, copy constructor, or copy assignment operator, you should probably implement all three.
  • A review of smart pointers, and how they can be used to avoid memory management issues like double-free errors.
  • Implementing copy semantics in classes by defining copy constructors.
  • The concept of copy assignment operators and how to properly handle self-assignment.
  • How copy semantics affect passing objects by value and the function return values.
  • An introduction to copy elision and Return Value Optimization (RVO) as compiler optimizations to reduce unnecessary copies

Was this lesson useful?

Next Lesson

Move Semantics

Learn how we can improve the performance of our types using move constructors, move assignment operators and std::move()
Illustration representing computer hardware
Ryan McCombe
Ryan McCombe
Updated
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Memory Management
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 124 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Move Semantics

Learn how we can improve the performance of our types using move constructors, move assignment operators and std::move()
Illustration representing computer hardware
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved