Copy constructors and copy assignment

Copying is the operation C++ does most often without being asked: a parameter passed by value, a return value, an element inserted into a vector, an assignment. The compiler writes a copy constructor and a copy assignment for every class, and for a class of values those are exactly right.

They stop being right when a class owns something. The default copy copies the pointer, and two objects then believe they own the same block. This lesson writes the deep copy that fixes it and shows the shared pointer that the default leaves behind.

The copy the compiler writes

For a class whose members are values, the compiler-generated copy constructor copies each member and the generated assignment assigns each member. The two objects are then independent: changing one does not touch the other.

The program copies a point and changes the copy, and the original keeps its values. The copy is a member-by-member copy, which is also what std::string does when it is copied: the characters are duplicated, not shared.

This is the behaviour every value type should have, and it is free. The rest of the lesson is about the classes where it is wrong.

#include <iostream> #include <string> class label { public: label(std::string text, int size) : text_(text), size_(size) {} void resize(int size) { size_ = size; } const std::string& text() const { return text_; } int size() const { return size_; } private: std::string text_; int size_; }; int main() { label first{"first", 10}; label second = first; second.resize(20); std::cout << first.text() << ' ' << first.size() << '\n'; std::cout << second.text() << ' ' << second.size() << '\n'; }

g++ -std=c++23 -Wall -Wextra default.cpp -o default_copy
./default_copy

first 10
first 20

A deep copy of an owned buffer

A class that allocates its own array needs a copy constructor that allocates a second array and copies the elements into it. The class below does that, and the copy prints its own line so the call is visible.

The alternative, copying the pointer, gives two objects one array: both destructors free it, and the second free is undefined behaviour. The program shows the correct version first; the last section shows what the pointer copy looks like.

Manual allocation is the subject of part 8, starting with lesson 089. Here new[] and delete[] appear only to give the copy something worth copying.

#include <iostream> class buffer { public: explicit buffer(int size) : size_(size), data_(new int[size]{}) { std::cout << "allocate " << size_ << '\n'; } buffer(const buffer& other) : size_(other.size_), data_(new int[other.size_]) { for (int i = 0; i < size_; ++i) { data_[i] = other.data_[i]; } std::cout << "copy " << size_ << '\n'; } ~buffer() { delete[] data_; std::cout << "free " << size_ << '\n'; } int& at(int index) { return data_[index]; } int at(int index) const { return data_[index]; } int size() const { return size_; } private: int size_; int* data_; }; int main() { buffer first{3}; first.at(0) = 7; buffer second = first; second.at(0) = 9; std::cout << first.at(0) << ' ' << second.at(0) << '\n'; }

g++ -std=c++23 -Wall -Wextra deep.cpp -o deep_copy
./deep_copy

allocate 3
copy 3
7 9
free 3
free 3

Copy assignment and self-assignment

Copy assignment replaces the contents of an object that already exists. It has to free the old buffer, copy the new contents and survive being called with the object itself: a = a must not free the data it is about to read.

The program's operator= checks this == &other first, then allocates the new array before freeing the old one, so a failed allocation leaves the object unchanged. That order is the copy-and-swap idea without a swap function.

Self-assignment is rare in ordinary code and common in generic code, where the two sides arrive through references and nobody checks. One comparison at the top costs nothing.

#include <iostream> class buffer { public: explicit buffer(int size) : size_(size), data_(new int[size]{}) {} buffer(const buffer& other) : size_(other.size_), data_(new int[other.size_]) { for (int i = 0; i < size_; ++i) { data_[i] = other.data_[i]; } } buffer& operator=(const buffer& other) { if (this == &other) { std::cout << "self-assignment\n"; return *this; } int* fresh = new int[other.size_]; for (int i = 0; i < other.size_; ++i) { fresh[i] = other.data_[i]; } delete[] data_; data_ = fresh; size_ = other.size_; return *this; } ~buffer() { delete[] data_; } int& at(int index) { return data_[index]; } int at(int index) const { return data_[index]; } private: int size_; int* data_; }; int main() { buffer a{2}; a.at(0) = 5; buffer b{2}; b.at(1) = 9; b = a; b = b; std::cout << b.at(0) << ' ' << b.at(1) << '\n'; }

g++ -std=c++23 -Wall -Wextra assignment.cpp -o assignment
./assignment

self-assignment
5 0

The rule of three

A class that needs a destructor to release something usually needs a copy constructor and a copy assignment too, because the default versions copy the handle rather than the resource. The three functions come as a set.

The program below writes all three and prints a line from each, so a copy, an assignment and a destruction are distinguishable in the output. It is the same buffer with the destructor, the copy constructor and the assignment in one class.

The rule is a rule of thumb, not a law: a class can own a resource and delete its copies instead, as lesson 027 did. What is not safe is owning a resource and writing none of the three.

#include <iostream> class tracker { public: explicit tracker(int value) : value_(value) {} tracker(const tracker& other) : value_(other.value_) { std::cout << "copy\n"; } tracker& operator=(const tracker& other) { value_ = other.value_; std::cout << "assign\n"; return *this; } ~tracker() { std::cout << "destroy " << value_ << '\n'; } private: int value_; }; int main() { tracker a{1}; tracker b{2}; b = a; { tracker c = a; } std::cout << "end\n"; }

g++ -std=c++23 -Wall -Wextra three.cpp -o three
./three

assign
copy
destroy 1
end
destroy 1
destroy 1

The copy that shares the buffer

Without a copy constructor the compiler copies the pointer, and the program below proves it: the two objects report the same address for their data. Both destructors will then delete the same array.

Nothing in the program warns about this, because sharing a pointer is legal C++. The failure comes at exit: the first destructor frees the array, the second frees the same address again, and the allocator aborts the process.

The output compares the two pointers and prints 1 for equal. The program then runs under GDB so the crash is visible: the backtrace puts the second free inside buffer::~buffer. Addresses and the musl source line change between builds and machines; the comparison and the frame names do not.

#include <iostream> class buffer { public: explicit buffer(int size) : data_(new int[size]{}) {} ~buffer() { delete[] data_; } const int* data() const { return data_; } private: int* data_; }; int main() { buffer first{3}; buffer second = first; std::cout << "same buffer: " << (first.data() == second.data()) << '\n'; }

g++ -g -std=c++23 -Wall -Wextra shared.cpp -o shared
gdb -q -batch -ex 'set disable-randomization off' -ex run -ex 'bt 3' ./shared

same buffer: 1

Program received signal SIGSEGV, Segmentation fault.
get_meta (p=p@entry=0x7b26dda93ab0 "") at src/malloc/mallocng/meta.h:141
warning: 141 src/malloc/mallocng/meta.h: No such file or directory
#0 get_meta (p=p@entry=0x7b26dda93ab0 "") at src/malloc/mallocng/meta.h:141
#1 0x00007b26dda32a55 in __libc_free (p=0x7b26dda93ab0) at src/malloc/mallocng/free.c:105
#2 0x0000602b5e29d393 in buffer::~buffer (this=0x7fffd0ea11d8) at shared.cpp:10

Exercises

  1. Add a size() accessor to the buffer in deep.cpp and print both sizes after the copy.
  2. Remove the self-assignment check from assignment.cpp and run b = b through a debugger. Where would the freed pointer be read?
  3. Write a copy constructor for shared.cpp that allocates its own array and check that the comparison prints 0.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. Which class gets a compiler-written copy constructor?

    Answer

    Every class, unless the program declares one

    The compiler generates the copy operations for a class that does not declare them.

  2. What does the default copy do with a pointer member?

    Answer

    It copies the pointer, so both objects point at one buffer

    The default is a member-by-member copy, and a pointer is a value that is copied as it is.

  3. Why does the assignment operator check for self-assignment?

    Answer

    It would free the buffer it is about to read

    Freeing first destroys the source when both names refer to the same object.

  4. What does the rule of three say?

    Answer

    If one of destructor, copy constructor and copy assignment is written, write all three

    The three manage the same resource, and a class that needs one usually needs the others.

  5. What does this program print? #include <iostream> struct holder { int* value; }; int main() { int* shared = new int{10}; holder first{shared}; holder second = first; std::cout << "first " << *first.value << '\n'; *second.value = 20; std::cout << "first " << *first.value << '\n'; delete shared; }

    Answer

    first 10 / first 20

    The default copy copies the pointer, so the write through the second object is visible through the first.

  6. What does this program print? #include <iostream> struct buffer { int* data; int size; buffer(int n) : data(new int[n]), size(n) { std::cout << "allocate " << n << '\n'; } buffer(const buffer& other) : data(new int[other.size]), size(other.size) { std::cout << "copy " << size << '\n'; for (int i = 0; i < size; ++i) data[i] = other.data[i]; } ~buffer() { std::cout << "free " << size << '\n'; delete[] data; } }; int main() { buffer first{3}; first.data[0] = 7; buffer second = first; second.data[0] = 9; std::cout << first.data[0] << ' ' << second.data[0] << '\n'; }

    Answer

    allocate 3 / copy 3 / 7 9 / free 3 / free 3

    The copy allocates its own buffer, the values differ, and both objects free their own block.

  7. What does this program print? #include <iostream> class box { public: box& operator=(const box& other) { if (this == &other) { std::cout << "self-assignment\n"; return *this; } value_ = other.value_; return *this; } int value_ = 5; }; int main() { box a; a = a; box b; std::cout << a.value_ << ' ' << b.value_ - 5; }

    Answer

    self-assignment / 5 0

    The check detects the same object and does nothing, and the values are unchanged.

  8. What is the order of the lines? #include <iostream> struct tracked { tracked() = default; tracked(const tracked&) { std::cout << "copy\n"; } tracked& operator=(const tracked&) { std::cout << "assign\n"; return *this; } ~tracked() { std::cout << "destroy 1\n"; } }; int main() { tracked first; tracked second; second = first; tracked third = first; std::cout << "end\n"; }

    Answer

    assign / copy / end / destroy 1 / destroy 1 / destroy 1

    The assignment and the copy print first, and the three objects are destroyed in reverse order after end.

  9. What happens to the program that copies an owning buffer with the default copy?

    Answer

    The two objects free the same block and the program crashes

    The shared buffer is freed twice, which the sanitizer or the allocator reports as a crash.

  10. What does a deep copy constructor have to do with the pointer?

    Answer

    Allocate new storage and copy the elements into it

    Each object must own its own block, or the destructors collide.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev