Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 7 of 12

Copying happens constantly, often without a visible =:

#include <iostream>

struct Point {
    int x;
    int y;
};

void take_by_value(Point p) {          // a copy is made here
    p.x = 999;
    std::cout << "inside the function, p.x = " << p.x << "\n";
}

Point make() {
    Point local{7, 8};
    return local;                      // and here
}

int main() {
    Point a{1, 2};
    Point b = a;                       // copy construction
    Point c{0, 0};
    c = a;                             // copy assignment

    b.x = 100;
    std::cout << a.x << " " << b.x << " " << c.x << "\n";

    take_by_value(a);
    std::cout << "after by-value call, a.x = " << a.x << "\n";

    Point d = make();
    std::cout << d.x << "," << d.y << "\n";
    return 0;
}
1 100 1
inside the function, p.x = 999
after by-value call, a.x = 1
7,8

Two distinct operations, and the difference matters:

  • Copy construction — building a new object from an existing one. Point b = a;
  • Copy assignment — overwriting an already existing object. c = a;

The first has no old state to clean up. The second does, which is what makes it harder.

What the compiler generates

If you write neither, the compiler produces both, and they copy each member in turn. For Point that is exactly right: copying two ints produces an independent Point.

For members that manage resources it is also right, because the member's own copy operations run. std::string and std::vector copy themselves properly, so a class made of them copies properly for free:

#include <iostream>
#include <string>
#include <vector>

struct Record {                        // no copy operations written
    std::string name;
    std::vector<int> scores;
};

int main() {
    Record a{"Sokha", {90, 85, 77}};
    Record b = a;                      // compiler-generated copy

    b.name = "Dara";
    b.scores[0] = 100;

    std::cout << a.name << " " << a.scores[0] << "\n";
    std::cout << b.name << " " << b.scores[0] << "\n";
    return 0;
}
Sokha 90
Dara 100

Fully independent, and not a line of copy code written. This is the normal case — most classes need nothing.

Where it goes wrong

The exception is a class holding a raw pointer it owns. Recall Buffer from lesson 9.4:

class Buffer {
    int* data;
    std::size_t count;
    // ...
};

Member-by-member copying copies the pointer, not what it points at. Two Buffers then hold the same address:

        b1                    b2  (a copy of b1)
   +----------+          +----------+
   | data ----+---+   +--+---- data |
   | count  4 |   |   |  | count  4 |
   +----------+   v   v  +----------+
              +------------+
              | 0 0 0 0    |     one array, two owners
              +------------+

Both destructors will run. Both will delete[] the same address. That is a double free — undefined behaviour, and in practice a crash or heap corruption.

This is a shallow copy: the handle is duplicated, the resource is not.

Writing the copy operations

#include <iostream>

class Buffer {
    int* data;
    std::size_t count;

public:
    Buffer(std::size_t n) : data(new int[n]), count(n) {
        for (std::size_t i = 0; i < count; ++i) {
            data[i] = 0;
        }
    }

    ~Buffer() { delete[] data; }

    // Copy constructor: allocate our own array, then copy the contents.
    Buffer(const Buffer& other) : data(new int[other.count]), count(other.count) {
        for (std::size_t i = 0; i < count; ++i) {
            data[i] = other.data[i];
        }
        std::cout << "deep-copied " << count << " ints\n";
    }

    // Copy assignment: we already own something, so release it first.
    Buffer& operator=(const Buffer& other) {
        if (this == &other) {
            return *this;                 // self-assignment: nothing to do
        }

        delete[] data;                    // release what we had
        count = other.count;
        data = new int[count];
        for (std::size_t i = 0; i < count; ++i) {
            data[i] = other.data[i];
        }
        std::cout << "deep-assigned " << count << " ints\n";
        return *this;
    }

    void set(std::size_t i, int v) { data[i] = v; }
    int get(std::size_t i) const { return data[i]; }
};

int main() {
    Buffer a(3);
    a.set(0, 42);

    Buffer b = a;                  // copy constructor
    b.set(0, 99);

    Buffer c(1);
    c = a;                         // copy assignment

    std::cout << a.get(0) << " " << b.get(0) << " " << c.get(0) << "\n";
    return 0;
}
deep-copied 3 ints
deep-assigned 3 ints
42 99 42

b changed without disturbing a. Three independent arrays, three clean destructor calls.

The four things copy assignment must get right

Compare the two signatures:

Buffer(const Buffer& other);              // copy constructor
Buffer& operator=(const Buffer& other);   // copy assignment

Copy assignment must additionally:

1. Handle self-assignment. Without the this == &other check, a = a; would delete[] data and then copy from the array it just freed — a use-after-free reading its own corpse.

2. Release the old resource. The object already owns something. Forget the delete[] and every assignment leaks.

3. Return Buffer&. So a = b = c; chains, matching how assignment works for built-in types.

4. Take a const&. Taking by value would need a copy to make the copy — infinite recursion.

Why const Buffer& and not Buffer

The copy constructor's parameter must be a reference. If it took Buffer other by value, making that parameter would require calling the copy constructor, which would require making its parameter, and so on. The compiler rejects it outright:

error: invalid constructor; you probably meant 'Buffer (const Buffer&)'

The better answer

Everything above is correct and about forty lines you should usually not write. Buffer exists to teach the mechanism. In real code:

#include <iostream>
#include <vector>

class Buffer {
    std::vector<int> data;              // the vector handles everything

public:
    Buffer(std::size_t n) : data(n, 0) {}

    void set(std::size_t i, int v) { data[i] = v; }
    int get(std::size_t i) const { return data[i]; }
    std::size_t size() const { return data.size(); }
};

int main() {
    Buffer a(3);
    a.set(0, 42);

    Buffer b = a;                       // correct, and no copy code written
    b.set(0, 99);

    std::cout << a.get(0) << " " << b.get(0) << "\n";
    return 0;
}
42 99

No destructor, no copy constructor, no copy assignment — and it is correct, exception-safe, and shorter. Lesson 9.11 names this the Rule of Zero.

Try it

Take the Buffer with the destructor but without the copy operations, and copy one. Predict what happens, then run it under -fsanitize=address.

Checkpoint

  • Copy construction builds a new object; copy assignment overwrites an existing one.
  • The compiler generates both, copying member by member — correct for values and for self-managing members.
  • A class owning a raw pointer gets a shallow copy: two owners, one resource, double free.
  • A copy constructor takes const T& — by value would recurse infinitely.
  • Copy assignment must check self-assignment, free the old resource, and return T&.
  • Holding a std::vector or std::string instead makes all of this unnecessary.

Questions about this lesson

Log in to ask a question.