C++ Part I · Module 13 · Copy and Move Semantics
The Copy Constructor
Chapter 13.1 established that the compiler writes a copy constructor for every type, that its answer is memberwise, and that for a class owning a raw resource the answer is a second owner for one buffer and a process killed by a signal. The repair is to write the function yourself, and that part is short — allocate storage of your own and copy the contents into it, which is four lines for a buffer class and measurably fixes the failure. The part that deserves a chapter is knowing when the function you just wrote will actually run, because a copy constructor is invoked in several situations where nothing in the source code contains the word copy or any other hint that an object is being duplicated. Passing an argument by value is one. Inserting into a container is another. And the one worth seeing before you meet it in a profiler is what happens when a container runs out of room: the elements it already holds have to be moved to larger storage, and for a type that can only be copied, that means copying every one of them again. Measured here, three insertions into a container that started with room for one produced six calls to the copy constructor, in code where the only thing written was push_back.
Intermediate17 min readC++Part IFundamentalsCopy Semantics
Part I · Module 13 · Chapter 13.2 · The Copy Constructor
1. The Signature
Buffer(const Buffer& other);A constructor taking a reference to const of its own type. Three things about that signature are forced rather than stylistic:
It takes a reference, not a value — taking the parameter by value would require copying it, which would call the copy constructor, which would need to copy its parameter. The recursion has no base case and the compiler rejects it.
It takes const, because copying should not modify the source, and because a const source must be copyable — Chapter 11.7 §3 measured what happens to a type whose operations are not const-callable.
And it takes its own type, which is what makes it the copy constructor rather than an ordinary converting constructor.
2. Writing One
For Chapter 13.1 §4's broken Buffer, the repair:
Buffer(const Buffer& o)
: size_(o.size_), data_(new int[o.size_]) // storage of our own
{
std::copy(o.data_, o.data_ + o.size_, data_); // then the contents
}Two steps: allocate, then copy the contents. The allocation happens in the member initializer list, which Chapter 12.3 established is where members are initialized rather than assigned.
Measured, with the same program that failed in Chapter 13.1 §4:
ctor data_=0x102c019a0
copy ctor data_=0x102c019b0 (from 0x102c019a0)
a.data()=0x102c019a0 b.data()=0x102c019b0 same? NO -- independent
after b.data()[0]=99: a[0]=0 b[0]=99
dtor data_=0x102c019b0
dtor data_=0x102c019a0
exit=0Two buffers, two destructors on two different pointers, exit 0 — and ASan reports nothing, where the generated copy gave double-free and exit 133.
Writing it was four lines. The rest of the chapter is the part that is not obvious.
3. When It Runs — Measured
Eight situations, with every call counted:
struct Probe {
int id;
explicit Probe(int i) : id(i) {}
Probe(const Probe& o) : id(o.id) { ++copies; std::printf(" COPY #%d\n", copies); }
};
static void byValue(Probe p) { (void)p; }
static void byConstRef(const Probe& p) { (void)p; }
static Probe returnsLocal() { Probe local(7); return local; }
static Probe returnsTemporary() { return Probe(8); }Measured:
1. Probe b = a; COPY #1
2. Probe c(a); COPY #2
3. byValue(a); COPY #3
4. byConstRef(a); (no copy)
5. returnsLocal(); (no copy)
6. returnsTemporary(); (no copy)
7. vector push_back(a); COPY #4
8. vector growth (reserve 1, push 3):
COPY #5 #6 #7 #8 #9 #10
total copies: 104. Reading the Results
Cases 1 and 2 are the same thing. Probe b = a; and Probe c(a); both construct a new object from an existing one. The = in the first is initialization, not assignment — a distinction Chapter 13.3 depends on.
Case 3 is the first hidden one. byValue(a) contains no mention of copying, and it copies. This is why Chapter 6.5 made const T& the default parameter form, and Chapter 9.6 §4 measured the allocation cost for a std::string argument.
Case 4 is the control. byConstRef(a) copied nothing. A reference binds to the existing object.
Cases 5 and 6 produced no copies at all, which looks surprising for functions that return an object by value. The compiler constructed the return value directly in the caller's storage instead of copying. Chapter 12.10 §7 measured this and found it survives -fno-elide-constructors for case 6 — it is mandatory there, and merely permitted for case 5. Chapter 13.12 is that distinction in full.
Case 7 is the second hidden one. v.push_back(a) copies the argument into the container's storage. Nothing says so.
5. Case 8: Six Copies for Three Insertions
std::vector<Probe> v;
v.reserve(1); // room for one
v.push_back(a);
v.push_back(a);
v.push_back(a);Six calls to the copy constructor for three insertions.
The mechanism is reallocation. A std::vector holds its elements in one contiguous block — Chapter 9.1's layout — so when it runs out of room it must obtain a larger block and put the existing elements there. For a type that can only be copied, "put them there" means copying every element it already holds, and then the old copies are destroyed.
So the three insertions cost: three copies of a into the container, plus three more copies of elements that were already in it, carried across two reallocations.
Three consequences worth keeping:
The cost is quadratic in the wrong conditions. Chapter 19.3 covers growth and reserve properly; the practical form is that reserve ahead of time removes the reallocation copies entirely — case 7 reserved 4 and cost exactly one copy.
It is invisible in the source. There is no syntax at the call site distinguishing a push_back that reallocates from one that does not.
And it is the single strongest argument for the second half of this module. If the elements could be transferred into the new storage rather than duplicated, those six copies become six pointer transfers with no allocation. Chapter 13.8 measures exactly that substitution: for four insertions of a 16-element buffer, 7 copies and 11 allocations became 0 copies, 7 moves and 4 allocations.
6. The Arithmetic of Growth
Section 5's six copies for three insertions invites the obvious question: how does it scale, and how much does reserve actually save? Measured, counting every copy and recording each capacity the container passed through:
| elements inserted | capacity steps | copies without reserve | copies with reserve(n) |
|---|---|---|---|
| 1 | 1 | 1 | 1 |
| 2 | 1 → 2 | 3 | 2 |
| 4 | 1 → 2 → 4 | 7 | 4 |
| 8 | 1 → 2 → 4 → 8 | 15 | 8 |
| 16 | 1 → 2 → 4 → 8 → 16 | 31 | 16 |
Two exact results fall out.
Without reserve, the cost is 2n − 1 copies for n elements. Each element is copied once on insertion, and the reallocations copy the existing elements again — and because the capacity doubles (1, 2, 4, 8, 16), the total extra work is n − 1.
With reserve(n), the cost is exactly n. One copy per element, no reallocation at all.
So the saving is a factor approaching two, which is worth putting in proportion: this is not a dramatic algorithmic improvement, and the doubling strategy is specifically designed to keep it bounded. Chapter 19.3 covers growth and reserve properly, including why doubling rather than fixed increments is what makes push_back cheap on average.
Three things to take from the table rather than the ratio.
The capacity sequence is observable, and that is how you find out what a container is doing without guessing. Printing capacity() after each insertion is the same instrument as the counting copy constructor, applied to the container instead of the element.
reserve is worth it when you know the count, and the measurement is the argument — 16 copies instead of 31 for a type where copying means an allocation. Chapter 9.6 §4 measured a std::string copy as a heap allocation past 22 characters, so for a container of strings those 15 extra copies are 15 extra allocations.
And the factor-of-two framing understates it for expensive elements. The copies removed are not the insertions — those happen either way — they are copies of elements that were already correctly in place. For a type holding a megabyte, reserve is the difference between touching that megabyte once and touching it several times.
7. The Summary Table
| Situation | Copy? |
|---|---|
T b = a; | yes |
T c(a); | yes |
f(a) where f takes T by value | yes |
f(a) where f takes const T& | no |
return localObject; | no — permitted elision (Chapter 13.12) |
return T(...); | no — mandatory elision |
v.push_back(a) | yes |
| a container reallocating | yes, once per existing element |
8. What This Function Is Not
A copy constructor constructs a new object. It is not the function that runs here:
Buffer a(4), b(2);
b = a; // NOT the copy constructorb already exists, so this is copy assignment — a different function with a different signature and one additional problem, which is that it must release whatever b was already holding before taking on a copy of a's. Chapter 13.3 is that function, and the asymmetry is worth noticing now: a copy constructor starts from nothing, and copy assignment starts from an object that already owns something.
The distinction is visible in the syntax only if you know to look:
Buffer b = a; // initialization -> copy CONSTRUCTOR
Buffer b; b = a; // assignment -> copy ASSIGNMENTThe = means different things in those two lines, which is a genuine wart and the reason the measured cases 1 and 2 above are the same operation written two ways.
9. What to Actually Do
Write a copy constructor only when the generated one is wrong — Chapter 13.1 §7's test.
Take const T&, never T by value.
Allocate in the initializer list, then copy the contents.
Expect a copy at every value boundary — by-value parameters, container insertion, container growth.
Pass by const T& (Chapter 6.5) so case 3 does not happen.
Use reserve when you know the size — case 7 cost one copy, case 8 cost six.
Count the copies when you suspect a cost. A temporary counting copy constructor answers it definitively.
And do not confuse it with copy assignment — T b = a; constructs, b = a; assigns.
10. Mistakes Worth Making Once
Taking the parameter by value. Infinite recursion; the compiler rejects it.
Omitting const. A const source then cannot be copied, which Chapter 11.7 §3 measured as making a type unusable.
Copying the pointer instead of the contents. Chapter 13.1 §4 — one buffer, exit 133.
Assuming push_back costs one copy. Measured: three insertions, six copies.
Assuming a by-value parameter is free. It is a copy, every time.
Relying on elision for a named local in general. Permitted, not guaranteed (Chapter 13.12).
Writing an expensive copy and relying on call sites to avoid it. They do not announce themselves.
Confusing T b = a; with assignment. It is initialization.
11. Interview Relevance
"What is the signature of a copy constructor and why?" T(const T&) — a reference because by value would recurse infinitely, and const so that a const source can be copied. The recursion argument is the one that shows understanding.
"Name the places a copy constructor is called." Initialization from an existing object, a by-value parameter, a by-value return in some cases, container insertion, and container reallocation. The last two are the ones that distinguish a real answer.
"How many copies does reserve(1) then three push_back cost?" Six, measured — three for the inserted elements and three more carried across reallocations. A memorable and very specific answer.
"Does returning by value copy?" Not for a returned temporary — that elision is mandatory. For a named local it is permitted and usually happens. Chapter 13.12's distinction.
"How would you find out whether your code is copying something expensive?" Add a counting copy constructor for one build. This is a practical answer that comes from having done it.
12. What You Should Be Able to Say Now
- The signature is
T(const T&)— a reference, because by value would recurse;const, so aconstsource is copyable. - Writing one is two steps: allocate in the initializer list, then copy the contents.
- Measured: the written copy gave two buffers, independent values, exit 0 and a clean ASan — where the generated one gave exit 133.
- Measured, eight situations, 10 copies total.
T b = a;andT c(a);are the same operation — both call the copy constructor.- A by-value parameter copies; a
const T¶meter does not. - Returning produced no copies — mandatory for a temporary, permitted for a named local (Chapter 13.12).
push_backcopies the argument.- Measured:
reserve(1)then threepush_backcost SIX copies — reallocation copies the elements already held. reserveahead of time removes those copies — the reserved case cost exactly one.- Measured exactly:
2n − 1copies withoutreserve, exactlynwith — 31 against 16 for sixteen elements, with the capacity doubling 1→2→4→8→16. - The saving is a factor approaching two, bounded on purpose by the doubling strategy (Chapter 19.3) — and larger in effect for elements whose copy allocates.
- None of the hidden call sites contains the word copy; all are boundaries an object crosses.
- A counting copy constructor is the instrument for finding hidden copies (Chapter 25.9).
- Case 8 is the argument for moving — Chapter 13.8 measured the same growth as 0 copies and 4 allocations instead of 7 and 11.
- A copy constructor is not copy assignment — one constructs, the other replaces (Chapter 13.3).
13. Exercises
Exercise 1 — write it. Give the copy constructor for a class holding char* text_ and int length_, and say which step goes in the initializer list.
Exercise 2 — count them. For Probe a(1); followed by Probe b = a; f(a); where f takes Probe by value, and g(a) where g takes const Probe& — how many copies?
Exercise 3 — predict. A std::vector<Probe> with no reserve, receiving four push_back(a) calls. Say whether the number of copies is four, and explain.
Exercise 4 — why const? Explain what breaks if the copy constructor takes T& instead of const T&, giving a concrete failing call.
Exercise 5 — why not by value? Explain the recursion, and say at which point the compiler stops you.
Exercise 6 — find the cost. A colleague's function is slow and takes its argument by value. Describe the one-build experiment that proves whether copying is the cause.
Exercise 7 — constructor or assignment? Classify each: T a = b;, T a; a = b;, T a{b};, f(b) with a by-value parameter, return b;.
Exercise 8 — remove the copies. Case 8 cost six copies. Give two independent changes that each reduce it, and say which chapter owns each.
14. Summary
T(const T&)— a reference because by value recurses,constso aconstsource can be copied.- Write it as allocate-then-copy, with the allocation in the initializer list.
- Measured: independent buffers, exit 0, ASan clean — against the generated copy's exit 133.
- Measured across eight situations, 10 copies. Initialization and
T c(a)are the same operation. - A by-value parameter copies;
const T&does not (Chapter 6.5). - Returns produced no copies — mandatory for a temporary, permitted for a named local.
- Measured: three
push_backs afterreserve(1)cost SIX copies, because reallocation copies existing elements. reserveremoves them; measured exactly2n − 1without againstnwith — 31 versus 16 for sixteen elements, capacity doubling.- The saving is a factor approaching two, bounded by design — and worth more when a copy allocates.
- The hidden call sites are boundaries — parameter, container, storage — and none says "copy".
- A counting copy constructor is how you find them, and case 8 is the argument for Chapter 13.8's move.
A copy constructor builds a new object out of an existing one, which means it starts from nothing and has nothing to clean up. The other half of copying does not have that luxury. When the destination already exists it is already holding a resource of its own, and taking on a copy of the source's means disposing of what it had — in the right order, without losing the thing it is copying from, and surviving the case where the source and the destination turn out to be the same object. The next chapter writes that function, and measures two reasonable-looking versions of it: one that silently produces garbage with a clean sanitizer report, and one that is a genuine use-after-free.
Standards & specifications
- Governing standard
- ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)
Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the C++ Programming curriculum.
