Copy Semantics: Deep vs Shallow
Learn the difference between shallow and deep copies, how to implement correct copy operations for resource-owning classes, and the copy-and-swap idiom.
Why Copy Semantics Matter
When you copy an object in C++, what exactly gets copied? For simple types, the answer is straightforward — all members are copied bit by bit. But when an object owns a resource through a pointer, the default (compiler-generated) copy constructor performs a shallow copy: it copies the pointer value but not the data it points to. This means two objects now share the same resource, leading to double-free bugs and data corruption. Rather like two people sharing one umbrella, and both trying to take it home.
Understanding copy semantics is essential because copies happen implicitly in many places: passing by value, returning from functions, inserting into containers, and assignment.
Shallow Copy Problem
The compiler-generated copy constructor copies each member. For pointers, this copies the address — not the pointed-to data. This is a shallow copy and it's dangerous for resource-owning classes.
#include <iostream>
#include <cstring>
class BrokenString {
char* data_;
std::size_t len_;
public:
BrokenString(const char* s)
: len_{std::strlen(s)}
, data_{new char[std::strlen(s) + 1]}
{
std::strcpy(data_, s);
}
~BrokenString() {
delete[] data_; // frees the buffer
}
// No copy constructor defined!
// Compiler generates shallow copy: copies the pointer
void print() const {
std::cout << data_ << '\n';
}
};
int main() {
BrokenString a{"Hello"};
{
BrokenString b = a; // shallow copy: b.data_ == a.data_
b.print(); // "Hello" — works for now
} // b destroyed: delete[] b.data_ (same pointer as a.data_!)
// a.data_ is now a dangling pointer — UNDEFINED BEHAVIOR
a.print(); // may crash, print garbage, or appear to work
return 0;
}Implementing Deep Copy
A deep copy allocates new memory and copies the actual data. This ensures each object owns its own independent copy of the resource. Everyone gets their own umbrella, and the queue for the door shortens considerably.
#include <iostream>
#include <cstring>
#include <algorithm>
class SafeString {
char* data_;
std::size_t len_;
public:
explicit SafeString(const char* s)
: len_{std::strlen(s)}
, data_{new char[std::strlen(s) + 1]}
{
std::strcpy(data_, s);
}
// Deep copy constructor
SafeString(const SafeString& other)
: len_{other.len_}
, data_{new char[other.len_ + 1]}
{
std::strcpy(data_, other.data_);
}
// Deep copy assignment with self-assignment check
SafeString& operator=(const SafeString& other) {
if (this == &other) return *this; // self-assignment guard
// Allocate new resource BEFORE releasing old one
char* new_data = new char[other.len_ + 1];
std::strcpy(new_data, other.data_);
// Release old resource
delete[] data_;
// Adopt new resource
data_ = new_data;
len_ = other.len_;
return *this;
}
~SafeString() { delete[] data_; }
void print() const {
std::cout << data_ << " (len=" << len_ << ")\n";
}
};
int main() {
SafeString a{"Hello"};
SafeString b = a; // deep copy: b gets its own buffer
SafeString c{"World"};
c = a; // deep copy assignment
a.print(); // Hello (len=5)
b.print(); // Hello (len=5)
c.print(); // Hello (len=5)
return 0;
}The Copy-and-Swap Idiom
The copy-and-swap idiom provides an elegant, exception-safe implementation of the copy assignment operator. It reuses the copy constructor and a swap function, providing strong exception safety with minimal code duplication.
#include <iostream>
#include <cstring>
#include <utility>
class Buffer {
char* data_;
std::size_t size_;
public:
explicit Buffer(std::size_t size)
: size_{size}, data_{new char[size]{}} {}
Buffer(const Buffer& other)
: size_{other.size_}, data_{new char[other.size_]}
{
std::memcpy(data_, other.data_, size_);
}
~Buffer() { delete[] data_; }
// Copy-and-swap: take parameter by VALUE (invokes copy ctor)
Buffer& operator=(Buffer other) { // copy happens here
swap(*this, other); // swap resources
return *this;
} // 'other' destroyed here, freeing our old resource
// Non-throwing swap — essential for strong exception safety
friend void swap(Buffer& a, Buffer& b) noexcept {
using std::swap;
swap(a.data_, b.data_);
swap(a.size_, b.size_);
}
char& operator[](std::size_t i) { return data_[i]; }
std::size_t size() const { return size_; }
};
int main() {
Buffer a{5};
a[0] = 'H'; a[1] = 'i';
Buffer b{10};
b = a; // copy-and-swap: exception-safe, self-assignment-safe
std::cout << b[0] << b[1] << " (size " << b.size() << ")\n";
return 0;
}Disabling Copy
Some types should not be copyable — for example, types that represent unique resources like file handles, network connections, or threads. Use =delete to clearly communicate and enforce this:
class UniqueConnection {
public:
UniqueConnection(const UniqueConnection&) = delete;
UniqueConnection& operator=(const UniqueConnection&) = delete;
// Move operations can still be enabled
};Before C++11, this was done by declaring copy operations private without a definition — a confusing hack that =delete elegantly replaces. Good riddance, frankly.
A naive copy assignment that deletes the old resource before allocating the new one is broken in two ways: (1) self-assignment (a = a) deletes the data then tries to copy from it, and (2) if new throws after delete, the object is left in an invalid state. The copy-and-swap idiom solves both problems: the copy is done before any modifications, and swap is noexcept. Quite a lot of damage for one innocent-looking line.
The best way to avoid copy bugs is to never manage raw resources directly. Use std::string instead of char*, std::vector instead of raw arrays, and std::unique_ptr instead of owning raw pointers. When all your members have correct copy semantics, the compiler-generated copy operations are automatically correct. This is the Rule of Zero in practice.
- Compiler-generated copy is shallow — it copies pointer values, not pointed-to data
- Resource-owning classes need a deep copy constructor and assignment operator
- The copy-and-swap idiom gives you exception safety and self-assignment safety for free
- Use
=deleteto explicitly forbid copying of unique-resource types - The best strategy is the Rule of Zero: use RAII wrappers so you never need custom copy logic
Quiz — Test Your Knowledge
(15 XP)1. What happens when you shallow-copy an object that owns heap memory through a raw pointer?
2. Why does the copy-and-swap idiom take the parameter by value?
3. Which approach is recommended to avoid needing custom copy operations altogether?