Shallow vs deep copying

When copying objects, C++ must decide how to copy each member. For simple classes, the default copying behavior works fine. However, for classes that manage resources like dynamic memory, we need to understand the difference between shallow and deep copying.

Shallow copying

The default copy constructor and assignment operator perform memberwise copying (also called shallow copying). Each member is copied individually:

#include <iostream>

class GameStats
{
private:
    int m_wins{};
    int m_losses{};

public:
    GameStats(int wins, int losses)
        : m_wins{wins}, m_losses{losses}
    {
    }

    friend std::ostream& operator<<(std::ostream& out, const GameStats& stats)
    {
        out << m_wins << " wins, " << m_losses << " losses";
        return out;
    }
};

int main()
{
    GameStats original{10, 3};
    GameStats copy{original};  // Default copy constructor does shallow copy

    std::cout << copy << '\n';

    return 0;
}

The compiler-generated copy constructor looks like:

GameStats(const GameStats& stats)
    : m_wins{stats.m_wins}
    , m_losses{stats.m_losses}
{
}

For this simple class, shallow copying works perfectly because we're just copying integer values.

The problem with shallow copying pointers

Shallow copying becomes problematic when a class contains pointers to dynamically allocated memory:

#include <iostream>
#include <cstring>

class PlayerProfile
{
private:
    char* m_username{};
    int m_score{};

public:
    PlayerProfile(const char* username, int score)
        : m_score{score}
    {
        m_username = new char[std::strlen(username) + 1];
        std::strcpy(m_username, username);
    }

    ~PlayerProfile()
    {
        delete[] m_username;
    }

    char* getUsername() const { return m_username; }
    int getScore() const { return m_score; }
};

int main()
{
    PlayerProfile player1{"Dragon_Slayer", 1500};

    {
        PlayerProfile player2{player1};  // Shallow copy!
        // player2.m_username points to the same memory as player1.m_username
    }  // player2 destroyed, deletes m_username

    // player1.m_username now points to deleted memory - UNDEFINED BEHAVIOR!
    std::cout << player1.getUsername() << '\n';  // Disaster!

    return 0;
}

What went wrong?

  1. player1 allocates memory and m_username points to it
  2. Default copy constructor copies the pointer value (shallow copy)
  3. Now both player1.m_username and player2.m_username point to the same memory
  4. When player2 is destroyed, its destructor deletes the shared memory
  5. player1.m_username is now a dangling pointer pointing to deleted memory
  6. Accessing player1.m_username causes undefined behavior

Deep copying

A deep copy allocates new memory and copies the actual values, not just the pointer:

#include <iostream>
#include <cstring>

class PlayerProfile
{
private:
    char* m_username{};
    int m_score{};

public:
    PlayerProfile(const char* username, int score)
        : m_score{score}
    {
        m_username = new char[std::strlen(username) + 1];
        std::strcpy(m_username, username);
    }

    // Deep copy constructor
    PlayerProfile(const PlayerProfile& player)
        : m_score{player.m_score}
    {
        if (player.m_username)
        {
            m_username = new char[std::strlen(player.m_username) + 1];
            std::strcpy(m_username, player.m_username);
        }
        else
        {
            m_username = nullptr;
        }
    }

    ~PlayerProfile()
    {
        delete[] m_username;
    }

    char* getUsername() const { return m_username; }
    int getScore() const { return m_score; }
};

int main()
{
    PlayerProfile player1{"Dragon_Slayer", 1500};

    {
        PlayerProfile player2{player1};  // Deep copy!
        // player2 has its own separate copy of the username
        std::cout << "Player 2: " << player2.getUsername() << '\n';
    }  // player2 destroyed, deletes its own memory

    // player1 still valid!
    std::cout << "Player 1: " << player1.getUsername() << '\n';

    return 0;
}

Deep copy assignment operator

We also need a deep copy assignment operator:

PlayerProfile& PlayerProfile::operator=(const PlayerProfile& player)
{
    if (this == &player)
        return *this;

    // Clean up existing resource
    delete[] m_username;

    // Copy non-pointer members
    m_score = player.m_score;

    // Deep copy the pointer member
    if (player.m_username)
    {
        m_username = new char[std::strlen(player.m_username) + 1];
        std::strcpy(m_username, player.m_username);
    }
    else
    {
        m_username = nullptr;
    }

    return *this;
}

Key differences from the copy constructor:

  1. Self-assignment check (to avoid deleting our own data)
  2. Delete existing allocated memory before allocating new memory
  3. Return *this for chaining

The rule of three

If a class requires any of the following, it probably requires all three:

  1. Destructor (to clean up resources)
  2. Copy constructor (to perform deep copy)
  3. Copy assignment operator (to perform deep copy)

If you're manually managing resources (like dynamic memory), you need all three to prevent resource leaks and undefined behavior.

A better solution: use the standard library

Writing correct copy constructors and assignment operators is error-prone. The standard library provides classes that handle memory management for you:

#include <iostream>
#include <string>

class PlayerProfile
{
private:
    std::string m_username{};  // std::string manages its own memory!
    int m_score{};

public:
    PlayerProfile(const std::string& username, int score)
        : m_username{username}, m_score{score}
    {
    }

    // No need for custom copy constructor, assignment operator, or destructor!
    // Compiler-generated versions work perfectly

    const std::string& getUsername() const { return m_username; }
    int getScore() const { return m_score; }
};

int main()
{
    PlayerProfile player1{"Dragon_Slayer", 1500};
    PlayerProfile player2{player1};  // Safe deep copy via std::string

    std::cout << "Player 1: " << player1.getUsername() << '\n';
    std::cout << "Player 2: " << player2.getUsername() << '\n';

    return 0;
}
Best Practice
Use standard library classes (std::string, std::vector, etc.) instead of managing memory yourself. They handle deep copying automatically and correctly.

Summary

Shallow copying: The default memberwise copying that copies member values including pointer values, resulting in multiple pointers to the same memory. Works fine for simple classes but causes problems with dynamically allocated resources.

Deep copying: Allocates new memory and copies the actual data, creating independent objects. Required when a class manages pointers to dynamically allocated memory.

The rule of three: If a class requires any one of the following, it probably requires all three: destructor, copy constructor, and copy assignment operator. This applies to classes that manually manage resources.

Copy constructor implementation: Must allocate new memory and copy the data (not just the pointer). Should handle null pointers and check for successful allocation.

Copy assignment implementation: Similar to copy constructor, but must also clean up existing resources, check for self-assignment, and return *this for chaining.

When implementing classes that manage resources, prefer using standard library classes like std::string and std::vector over manual memory management. They automatically handle deep copying correctly and reduce the likelihood of memory leaks and undefined behavior.