Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
deepcopy.cpp — the copy constructor
    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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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=0

Two 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:

Azvya Education Pvt. Ltd.VLSI Mentor
whencopy.cpp — the probe
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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: 10

4. 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    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 insertedcapacity stepscopies without reservecopies with reserve(n)
1111
21 → 232
41 → 2 → 474
81 → 2 → 4 → 8158
161 → 2 → 4 → 8 → 163116

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

SituationCopy?
T b = a;yes
T c(a);yes
f(a) where f takes T by valueyes
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 reallocatingyes, 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Buffer a(4), b(2);
    b = a;                      // NOT the copy constructor

b 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    Buffer b = a;               // initialization  -> copy CONSTRUCTOR
    Buffer b; b = a;            // assignment      -> copy ASSIGNMENT

The = 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

  1. The signature is T(const T&) — a reference, because by value would recurse; const, so a const source is copyable.
  2. Writing one is two steps: allocate in the initializer list, then copy the contents.
  3. Measured: the written copy gave two buffers, independent values, exit 0 and a clean ASan — where the generated one gave exit 133.
  4. Measured, eight situations, 10 copies total.
  5. T b = a; and T c(a); are the same operation — both call the copy constructor.
  6. A by-value parameter copies; a const T& parameter does not.
  7. Returning produced no copies — mandatory for a temporary, permitted for a named local (Chapter 13.12).
  8. push_back copies the argument.
  9. Measured: reserve(1) then three push_back cost SIX copies — reallocation copies the elements already held.
  10. reserve ahead of time removes those copies — the reserved case cost exactly one.
  11. Measured exactly: 2n − 1 copies without reserve, exactly n with — 31 against 16 for sixteen elements, with the capacity doubling 1→2→4→8→16.
  12. 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.
  13. None of the hidden call sites contains the word copy; all are boundaries an object crosses.
  14. A counting copy constructor is the instrument for finding hidden copies (Chapter 25.9).
  15. 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.
  16. 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, const so a const source 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 after reserve(1) cost SIX copies, because reallocation copies existing elements.
  • reserve removes them; measured exactly 2n − 1 without against n with — 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.