Skip to content

Intermediate · Classes, constructors and RAII · topic 16 of 42 · 30 min

Copying objects: the rule of three, five and zero

Explanation

When you copy an object, C++ calls its copy constructor or copy assignment operator. The compiler-made versions copy member by member, which is wrong for a raw pointer (both copies share and delete the same memory). The rule of zero says: use members that manage themselves (std::vector, std::string, smart pointers) so you need to write none of these functions.

The special member functions

Besides constructors and the destructor, C++ can generate four more special functions for a class: the copy constructor (make a new object as a copy), the copy assignment operator (overwrite an existing object with a copy), and their move versions (move constructor and move assignment), which you will meet in the move semantics topic.

cpp
Student a("Asha", 1);
Student b = a;        // copy constructor: b is a new copy of a
Student c("Ravi", 2);
c = a;                // copy assignment: c already existed, now it becomes a copy of a
void enrol(Student s);
enrol(a);             // copy constructor again: pass by value makes a copy
Copies happen more often than you might think: every pass by value, every return of a named local (unless optimised) and every push_back of an lvalue.

Shallow copy: the raw pointer disaster

cpp
class Buffer {
public:
    explicit Buffer(int n) : size_(n), data_(new int[n]{}) {}
    ~Buffer() { delete[] data_; }
private:
    int size_;
    int* data_;
};

void oops() {
    Buffer a(100);
    Buffer b = a;   // default copy copies the POINTER, not the 100 ints
}                   // b's destructor deletes the array, then a's destructor deletes it AGAIN: crash
The compiler-generated copy is a shallow copy: member by member, so both objects point to the same heap array.
Think of it like this: A shallow copy is like photocopying the house key instead of building a second house. Now two people "own" one house; when the first moves out and demolishes it, the second comes home to rubble. A deep copy builds a second, identical house.

The rule of three

If your class needs a custom destructor (because it owns a resource), it almost certainly also needs a custom copy constructor and copy assignment operator. These three go together: that is the rule of three. In C++11 and later, add the move constructor and move assignment and it becomes the rule of five.

cpp
class Buffer {
public:
    explicit Buffer(int n) : size_(n), data_(new int[n]{}) {}
    ~Buffer() { delete[] data_; }

    Buffer(const Buffer& other)                       // deep copy
        : size_(other.size_), data_(new int[other.size_]) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    Buffer& operator=(const Buffer& other) {          // copy-and-swap would also work
        if (this != &other) {
            int* fresh = new int[other.size_];
            std::copy(other.data_, other.data_ + other.size_, fresh);
            delete[] data_;
            data_ = fresh;
            size_ = other.size_;
        }
        return *this;
    }
private:
    int size_;
    int* data_;
};
Correct, but long and easy to get wrong. That is why the rule of zero exists.

The rule of zero (what you should usually do)

cpp
#include <vector>

class Buffer {
public:
    explicit Buffer(int n) : data_(n) {}
    int size() const { return static_cast<int>(data_.size()); }
private:
    std::vector<int> data_;   // vector already knows how to copy, move and free itself
};
No destructor, no copy or move functions: the compiler-generated ones do the right thing because every member manages itself.

The rule of zero: design classes so they need none of the five special functions, by building them from members that already handle their own resources (std::string, std::vector, std::unique_ptr, std::shared_ptr). Only low-level "resource handle" classes, like the File class in the RAII topic, should write them.

= default and = delete

  • = default asks the compiler to generate the normal version, for example Buffer(const Buffer&) = default;.
  • = delete forbids an operation: Buffer(const Buffer&) = delete; makes copying a compile error. Use it for things that must have one owner (files, sockets, mutexes).
Careful: In copy assignment, always handle self-assignment (a = a). The naive version deletes its own data first and then copies from the deleted memory. Check if (this != &other), or use the copy-and-swap idiom.
Tip: Ask three questions for every class: Does it own a resource? Should copying be allowed? If copying is allowed, should it be deep? If you can answer "no resource, members manage themselves", you are in rule-of-zero land and are done.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Copying objects: the rule of three, five and zero · 1/6

Copying objects

Copying objects looks simple, but it hides a classic C plus plus bug. Let us see it and fix it.

Prefer a video by a teacher? Find free videos on “c++ rule of three rule of five rule of zero copy constructor deep copy” on YouTube ↗

In real life: Copying a photo in an image editor

A mobile photo editor keeps each image's pixels on the heap. When you tap "duplicate layer", the program must make a deep copy, so that editing the duplicate does not change the original. Storing pixels in a std::vector<std::uint8_t> instead of a raw pointer gives correct deep copies automatically.

cpp
#include <cstdint>
#include <iostream>
#include <vector>

class Image {
public:
    Image(int w, int h) : w_(w), h_(h), pixels_(static_cast<std::size_t>(w) * h, 0) {}
    void brighten(std::uint8_t by) { for (auto& p : pixels_) p = static_cast<std::uint8_t>(p + by); }
    int firstPixel() const { return pixels_[0]; }
private:
    int w_, h_;
    std::vector<std::uint8_t> pixels_;
};

int main() {
    Image original(1920, 1080);
    Image layer = original;      // deep copy, thanks to vector
    layer.brighten(40);
    std::cout << original.firstPixel() << ' ' << layer.firstPixel() << '\n';   // 0 40
}
Rule of zero: no special member functions written, and copies are still correct.

Practice (0/4 done)

Task 1 · Question

Student b = a; calls which function?

Task 2 · Question

A class has int* data_ (allocated with new[]) and a destructor that calls delete[] data_, but no copy constructor. What happens when an object is copied and both go out of scope?

Task 3 · Question

Which class follows the rule of zero?

Task 4 · Practice

Rewrite this class using the rule of zero, then explain in two sentences why the original crashes when copied: class Playlist { public: Playlist() : songs_(new std::string[50]) {} ~Playlist() { delete[] songs_; } private: std::string* songs_; };

Quick quiz

Question 1

1.What does the rule of three say?

Question 2

2.What does Socket(const Socket&) = delete; do?

Question 3

3.Why must copy assignment check this != &other?

Question 4

4.Which pair makes a deep copy automatically when you copy the object?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does the rule of three say?

Answer: If a class needs a custom destructor, copy constructor or copy assignment, it probably needs all three

These three manage the same resource, so they come as a set.

What does Socket(const Socket&) = delete; do?

Answer: Makes copying a Socket a compile error

= delete removes the operation, so any attempt to copy fails to compile.

Why must copy assignment check this != &other?

Answer: So that a = a does not free its own data before copying from it

Self-assignment with delete-then-copy reads freed memory.

Which pair makes a deep copy automatically when you copy the object?

Answer: A std::vector member

std::vector copies all its elements into new memory.

How do you pass ownership of a unique_ptr into a function sink(std::unique_ptr<T> p)?Related

Answer: sink(std::move(p))

Ownership must be moved explicitly.

From Smart pointers 1: std::unique_ptr

You wrote Student(std::string name, int roll). What happens with Student s;?Related

Answer: Compile error: no default constructor exists

Writing any constructor stops the compiler from generating the default one.

From Constructors and destructors

A class declares only a destructor (~T() {}) and holds a std::vector. What happens to its moves?Related

Answer: The implicit move operations are not declared, so moves silently become copies

A user-declared destructor suppresses the implicit move constructor and move assignment.

From Move semantics: steal instead of copy

Why prefer std::make_shared over std::shared_ptr<T>(new T)?Related

Answer: One allocation for object and control block, and no raw new in your code

make_shared is faster and safer.

From Smart pointers 2: std::shared_ptr and std::weak_ptr

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Classes, constructors and RAII”. The module exam comes next.
  2. Module 4 exam: Classes, constructors and RAII · 12 questions · 20 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Operator overloading, inheritance and polymorphism. 42 topics left in the course before the final exam.

← Previous: RAII: let objects clean up for you