C++ Part I · Module 13 · Copy and Move Semantics
Shallow Copy, Deep Copy and the Double Free That Follows
Three chapters have now described this failure and none of them has drawn it, which is a real omission because the bug is fundamentally a picture. Two objects exist. Each of them contains a pointer. Each of their destructors releases what its pointer points at. And there is one buffer. Every symptom follows from that arrangement: why there is no compiler diagnostic, since nothing about the arrangement is illegal; why the program runs correctly for as long as both objects are alive, since reading and writing through either pointer works perfectly; why the crash arrives at a closing brace rather than at the copy that caused it; and why the sanitizer names the destructor as the site when the defect is in a function thirty lines away. This chapter draws the arrangement, traces the whole sequence event by event from the copy to the signal, and then puts the corrected version beside it so the difference is visible as a change in the picture rather than a change in the code — two arrows into one buffer becoming one arrow each into two. The two terms for those pictures are the ones in this chapter's title, and they are worth having precisely, because shallow is not a synonym for wrong.
Intermediate18 min readC++Part IFundamentalsCopy Semantics
Part I · Module 13 · Chapter 13.4 · Shallow Copy, Deep Copy and the Double Free That Follows
1. Two Terms
A shallow copy duplicates the members. A deep copy duplicates the members and whatever they point at.
Chapter 13.1 §2 established that the compiler's generated copy is memberwise, which makes it shallow by definition — it copies each member, and copying a pointer produces another pointer to the same place.
Two things follow that are worth getting straight before the trace.
Shallow is not a synonym for wrong. For a type whose members own nothing, shallow is the only sensible meaning of copying and there is nothing deeper to do. Chapter 13.1 §3 measured a struct of an int, a double and a std::string copying correctly — and that copy was shallow at the top level, with the depth supplied by std::string's own copy constructor one level down.
And deep is not automatically right either. A deep copy of an object holding a pointer to something it does not own would duplicate something it has no business duplicating. Chapter 10.9 §5's distinction decides which you want: an owning pointer wants depth, a borrowed one wants shallow.
So the question is never "shallow or deep" in the abstract. It is Chapter 13.1 §7's question — does this type own what its members point at — and the answer determines which is correct.
2. The Arrangement
The class from Chapter 13.1 §4, with the generated copy:
class Buffer {
public:
explicit Buffer(int n) : size_(n), data_(new int[n]) { }
~Buffer() { delete[] data_; }
private:
int size_;
int* data_;
};
Buffer a(4);
Buffer b = a; // shallow: b.data_ == a.data_Two arrows, one buffer. That is the whole bug, and §3 to §6 are consequences of it.
3. Why Nothing Objects
The arrangement in the figure is legal in every respect:
Two pointers to one object is permitted, and common, and necessary. Chapter 10.9 §5 called it borrowing; a const T& parameter is exactly this.
The copy is a valid operation. Chapter 13.1 §5 traced it: a pointer's copy constructor copies the pointer, which is correct.
And both objects work. Reading a.data()[0] and b.data()[0] both reach the buffer and both give the right answer. The program is fully functional while both objects are alive.
So there is nothing for the compiler to report, and measured: 0 diagnostics at the standing flags. The defect is not in any single operation; it is in the combination of a copy that shares and a destructor that releases.
4. The Trace
Measured, with stdout unbuffered so the output survives the abort:
creating a:
ctor data_=0x102e6d9a0
copying a into b (compiler-generated copy):
a.data()=0x102e6d9a0 b.data()=0x102e6d9a0 same? YES -- ONE buffer, TWO owners
leaving scope (two destructors on one buffer):
dtor data_=0x102e6d9a0 delete[]
dtor data_=0x102e6d9a0 delete[]
exit=133Event by event:
1. Buffer a(4) — the constructor allocates. One buffer exists, at …9a0, with one owner.
2. Buffer b = a — the generated copy constructor copies size_ and copies data_. Now two objects hold …9a0. Nothing is wrong yet, and nothing has been reported.
3. Both objects are used normally. Any amount of correct work can happen here. In a real program this phase may last the whole run.
4. The closing brace. Chapter 12.8 established destruction in reverse order of construction, so b is destroyed first. Its destructor calls delete[] on …9a0. The buffer is now released, and a still holds its address — at this instant a is exactly Chapter 10.7's dangling pointer.
5. a's destructor runs and calls delete[] on …9a0 again. The allocator detects a block already free and aborts: exit 133, which Chapter 10.7 §5 identified as SIGTRAP.
Under AddressSanitizer:
ERROR: AddressSanitizer: attempting double-free on 0x6020000000d0
SUMMARY: AddressSanitizer: double-free rawowner.cpp:8 in Buffer::~Buffer()5. Three Things the Trace Explains
Why the crash is at a closing brace. The copy is the cause and step 4 is the trigger. Between them the program is correct, so a debugger stopped at the crash shows you a destructor doing exactly what it was written to do. Chapter 2.7's advice about reading the first error has a runtime analogue: the place it failed is not the place it is wrong.
Why the sanitizer names the destructor. ASan reports where the invalid operation happened, and the invalid operation is the second delete[]. That is the honest site. Chapter 10.2 §8 made the same observation in reverse — the compiler warning points at where the bad address was produced, the sanitizer at where it was used.
And why step 4 is already a bug even without step 5. After b's destructor, a holds a dangling pointer. If a were used rather than destroyed, that is Chapter 10.7 §4's use-after-free — measured there as reading 5 instead of 42, silently, exit 0. The double free is the loud version of this arrangement; the quiet version is worse.
6. The Deep Copy
Chapter 13.2 §2's repair, measured on the same program:
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=0ASan: clean.
In terms of the figure, exactly one thing changed: there are now two buffer boxes, and one arrow into each. The two objects, the two pointers and the two destructors are all still there. What was removed is the sharing.
And the independence is measurable, not just structural: writing 99 through b left a[0] at 0. Chapter 13.1 §3's value type behaved the same way, for the same reason.
Note also the destruction order in that output — …9b0 before …9a0, reverse of creation, which is Chapter 12.8's guarantee doing its job. With two separate buffers the order is irrelevant; with one shared buffer it determined which object became the dangling one.
7. The Two Pictures Side by Side
| Shallow (generated) | Deep (written) | |
|---|---|---|
a.data_ | …9a0 | …9a0 |
b.data_ | …9a0 | …9b0 |
| buffers | 1 | 2 |
| destructors | 2 | 2 |
releases of …9a0 | 2 | 1 |
| compiler diagnostics | 0 | 0 |
| exit | 133 | 0 |
| ASan | double-free | clean |
The number of destructors did not change. Both versions destroy two objects and call delete[] twice. The difference is entirely in how many buffers those two calls are aimed at.
That reframing is useful when reading unfamiliar code: the question is never "is this destructor correct" in isolation, but how many objects can reach this resource, and how many of them think they own it.
8. What This Does Not Fix
The deep copy repairs construction from an existing object. It says nothing about assignment:
Buffer a(4), b(2);
b = a; // still the generated copy assignmentThe generated assignment is also memberwise, so it produces the same shared arrangement and leaks b's original buffer — Chapter 13.3 §8 measured both, with a live-buffer counter showing 1 still live after both objects were destroyed.
So a class with a destructor and a hand-written copy constructor and a generated copy assignment is still broken, in a different way. That observation is the whole of Chapter 13.5, which is the rule that these functions come as a set.
9. When Two Owners Is the Design
One honest qualification, because the chapter has treated shared ownership as the bug.
Sometimes several objects genuinely should share one resource, and the problem in §2 is not the sharing — it is that nothing was counting. If the buffer were released when the last owner went away rather than when each owner went away, the arrangement in the figure would be correct.
That is reference counting, and it is std::shared_ptr — Chapter 14.5. It is the right answer for a genuinely shared resource and the wrong answer for most types, because it costs bookkeeping and because most resources have one owner. Chapter 10.9 §5 listed the three patterns: exclusive, shared, and borrowing, and noted that most pointers should be borrowing.
And there is a third option that neither duplicates nor shares: transfer the buffer from a to b, leaving a with nothing to release. That is a move, it is the second half of this module, and Chapter 13.8 measures it as the cheapest of the three.
10. What to Actually Do
Ask how many objects can reach each resource, and how many will release it. One releaser is correct; one per reacher is the bug.
Write a deep copy when the class owns what its pointer points at (Chapter 13.2).
Leave the copy shallow when the pointer is borrowed — and then make sure nothing in the class releases it.
Expect no diagnostic for the bug — measured: 0.
Expect the crash at a closing brace, not at the copy.
Read the sanitizer's site as where it failed, not where it is wrong.
Fix copy assignment too — a deep copy constructor alone leaves Chapter 13.3's leak (Chapter 13.5).
Consider the other two options: shared_ptr when sharing is the design (Chapter 14.5), and a move when transfer is what you want (Chapter 13.8).
And prefer a self-managing member, which draws the right picture for you.
11. Mistakes Worth Making Once
Treating "shallow" as a synonym for "wrong". It is correct for a type that owns nothing, and for a borrowed pointer.
Treating "deep" as automatically right. A deep copy of a borrowed pointer duplicates something you do not own.
Looking for the bug at the crash site. It is in the copy; the crash is in the destructor.
Expecting a compiler warning. Measured: 0 diagnostics.
Concluding the code is fine because it ran. It is correct until the first destructor.
Fixing the copy constructor and stopping. Copy assignment is still generated — Chapter 13.3 §8's leak.
Reaching for shared_ptr to silence a double free. That is correct only if sharing is genuinely the design.
Forgetting that step 4 alone is a dangling pointer — the quiet version of this bug is a use-after-free.
12. Interview Relevance
"What is the difference between a shallow and a deep copy?" Shallow duplicates the members; deep duplicates the members and their targets. The answer that lands adds that the compiler's generated copy is shallow by construction, and that shallow is correct whenever the members own nothing.
"Walk me through the double free." Allocate, copy sharing the pointer, both objects valid, first destructor releases, second object now dangling, second destructor releases again — exit 133. The step-by-step is what is being asked for.
"Where does the crash happen, and where is the bug?" The crash is in the destructor; the bug is in the copy. A common and genuinely useful distinction.
"Why no compiler warning?" Because every individual operation is legal — two pointers to one object is a normal, necessary thing. Only the combination with a releasing destructor is wrong.
"Is a deep copy constructor enough?" No — copy assignment is still generated, and it both shares and leaks. That answer leads naturally to the Rule of Three.
"When would two owners be correct?" With reference counting, so the last one releases — shared_ptr. Knowing when sharing is right is better than reflexively calling it a bug.
13. What You Should Be Able to Say Now
- Shallow duplicates members; deep duplicates members and what they point at.
- The generated copy is shallow by definition — memberwise (Chapter 13.1 §2).
- Shallow is not wrong for a type owning nothing, and deep is not right for a borrowed pointer.
- The bug is the arrangement: two objects, one buffer, two destructors that each release.
- Nothing objects — two pointers to one object is legal, the copy is legal, both objects work. Measured: 0 diagnostics.
- The trace: allocate → shallow copy → normal use → first destructor releases → the other object is now dangling → second destructor releases again → exit 133.
- ASan reports
double-free … in Buffer::~Buffer()— the honest site. - The crash is at a closing brace and the bug is in the copy.
- Step 4 alone is a dangling pointer — the quiet version is Chapter 10.7's use-after-free, measured as a wrong value with exit 0.
- The deep copy changes one thing in the picture: two buffers, one arrow each. Measured independent, exit 0, ASan clean.
- The number of destructors did not change — only how many buffers they aim at.
- A deep copy constructor alone is insufficient — generated copy assignment still shares and leaks (Chapter 13.5).
- Four arrangements exist, and only several reachers, each releasing is wrong.
- Shared ownership with counting is
shared_ptr(Chapter 14.5); transfer is a move (Chapter 13.8).
14. Exercises
Exercise 1 — draw it. For Buffer a(4); Buffer b = a; Buffer c = b; draw the arrangement and say how many releases the one buffer receives.
Exercise 2 — trace it. Write the five events from Buffer b = a; to the signal, and name the step at which a dangling pointer first exists.
Exercise 3 — classify. For each, say which of §9's four arrangements it is: a const T& parameter; a vector<T*> of objects owned elsewhere; two shared_ptr to one object; two raw-pointer owners.
Exercise 4 — shallow or deep? Decide for each member: int size_; int* ownedBuffer_; Logger* borrowedLogger_; std::string name_; FILE* ownedFile_.
Exercise 5 — the quiet version. Modify §4's program so it produces a use-after-free instead of a double free, and say what Chapter 10.7 measured that to look like.
Exercise 6 — why no warning? Explain to a colleague why -Wall -Wextra says nothing, in terms of which operations are legal.
Exercise 7 — incomplete fix. A class has a destructor and a deep copy constructor. Give a three-line main that still misbehaves, and say which function is missing.
Exercise 8 — the other two options. For a 10 MB buffer being handed from a producer to a consumer, compare deep copy, shared_ptr and move, and say which you would choose.
15. Summary
- Shallow duplicates members; deep also duplicates their targets. The generated copy is shallow by construction.
- Neither is right in the abstract — ownership decides.
- The bug is a picture: two objects, one buffer, two releasing destructors.
- Every individual operation is legal, so measured: 0 diagnostics.
- The trace: allocate → copy → correct use → first release → other object dangling → second release → exit 133; ASan
double-free … in ~Buffer(). - The crash is at a brace; the bug is in the copy.
- The deep copy changes one thing: two buffers, one arrow each — independent, exit 0, ASan clean.
- Two destructors either way — only the aim changed.
- A deep copy constructor is not enough; generated assignment still shares and leaks.
- Only "several reachers, each releasing" is wrong — borrowing, counting and exclusive ownership are all correct.
The repair is now complete for construction and incomplete for the type as a whole, and the pattern in that sentence is the subject of the next chapter. A class that needed a destructor needed it because it owns something; a class that owns something cannot use the generated copy constructor; and a class with a hand-written copy constructor and a generated copy assignment is the broken arrangement again with a leak added. Those three functions are not three independent decisions — they are three answers to one question, and getting one right while leaving the others to the compiler produces a type that is wrong in a way that is harder to find than the original. The next chapter states that as a rule, with the compiler's own opinion on the matter, which turns out to be a warning that says so in as many words.
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.
