C++ Part I · Module 25 · Debugging, Undefined Behavior, and Performance
Lifetime Failures: Dangling, Use-After-Free and Invalidated Iterators
A dangling pointer, a use after free and an invalidated iterator are usually taught as three bugs. They are one bug with three locations: an object's lifetime ended and something still refers to it. Recognising that is practical rather than tidy, because the reasoning that finds all three is the same reasoning — who owns this object, when does that owner's lifetime end, and does anything outlive it — which is the question Modules 10 and 12 were about. The chapter measures four instances of the single bug and then measures which tools report each, and the result is the most useful thing in it: the compiler's warnings and the address sanitiser are almost exactly complementary, each catching the cases the other is silent about, and one of the four is reported by no run-time configuration available on this machine. That pattern is why the answer to this bug class is a build configuration with both, plus the ownership discipline that makes the dangling reference impossible to write.
Advanced18 min readC++Part IFundamentalsDebugging
Part I · Module 25 · Chapter 25.4 · Lifetime Failures: Dangling, Use-After-Free and Invalidated Iterators
1. One Bug, Three Locations
An object's lifetime begins when its initialisation completes and ends when its destructor starts — or, for an object with dynamic storage, when the storage is deallocated. Accessing an object outside its lifetime is undefined behaviour, and that single rule covers all of this chapter:
| the name it is given | where the object lived | what ended its lifetime |
|---|---|---|
| dangling pointer / reference | the stack | the enclosing scope exited |
| use after free | the heap | delete or free |
| invalidated iterator | a container's buffer | the container reallocated or erased |
dangling string_view / c_str | a temporary or a local | the owner was destroyed |
Nothing about the mechanism differs. The pointer or iterator still holds an address; the storage at that address is no longer the object it referred to; reading it reads whatever is there now.
What differs between them is who was responsible for the lifetime, and that is what makes them feel like separate bugs. In the stack case it is the scope. In the heap case it is whoever called delete. In the container case it is a method you called on the container, which is the one people find surprising — push_back ends the lifetime of every element at its old address, and the standard says so.
Chapter 12's ownership question is therefore the diagnostic question for all four: who owns this, and does anything outlive the owner?
2. The Four Instances
// 1. a dangling pointer to an automatic object
const int* danglingLocal() { int local = 0x1234; return &local; }
// 2. use after free
int useAfterFree() { int* p = new int(7); delete p; return *p; }
// 3. an invalidated iterator
int invalidatedIterator()
{
std::vector<int> v{1,2,3};
auto it = v.begin();
v.push_back(4); // may reallocate -> it dangles
return *it;
}
// 4. a dangling reference into a destroyed string
const char* danglingTemp() { std::string s = "SLVERR"; return s.c_str(); }Run plainly at -O1, all four produce output and the process exits 0. The values they print are garbage and are not reproducible between runs, so there is no figure to quote — which is itself the first thing to notice about this bug class. Chapter 25.2 §3's uninitialised values were stable; these are not, because what occupies freed heap or a dead frame depends on everything the program has done since.
3. Which Tools Report Which
The measurement that organises the chapter. Each case isolated into its own program, so no abort masks a later one:
-Wall -Wextra | plain run | ASan (default) | ASan + detect_stack_use_after_return=1 | UBSan | |
|---|---|---|---|---|---|
| 1. dangling local | CATCH | silent, exit 0 | miss | CATCH — stack-use-after-return, exit 134 | miss |
| 2. use after free | — | silent, exit 0 | CATCH — heap-use-after-free, exit 134 | CATCH | miss |
| 3. invalidated iterator | — | silent, exit 0 | CATCH — heap-use-after-free, exit 134 | CATCH | miss |
4. dangling c_str | CATCH | silent, exit 0 | miss | miss | miss |
The compile-time half is exactly two warnings, and both are the same diagnostic:
three.cpp:10:13: warning: address of stack memory associated with local variable 'local' returned [-Wreturn-stack-address]
three.cpp:34:12: warning: address of stack memory associated with local variable 's' returned [-Wreturn-stack-address]-Wall -Wextra caught the two stack cases and said nothing about the two heap cases. ASan, with its default options, caught the two heap cases and nothing about the two stack cases. The two detectors partition the four bugs between them with no overlap.
Three details inside that table matter.
detect_stack_use_after_return is off by default, and turning it on is what makes ASan catch case 1:
ERROR: AddressSanitizer: stack-use-after-returnIt is off because it is expensive — it has to keep frames alive after they return — which is a defensible default and a bad one for a debug build you are already running sanitised.
Case 4 is caught by nothing at run time. Not by ASan with either setting, not by UBSan. Its only detector on this machine is the compile-time warning, which is why the warning's presence in your build is not optional.
UBSan caught none of the four. It is not a memory tool; Chapter 25.3 §4's callout explains what each sanitiser's source of truth is, and lifetime is not among UBSan's.
4. Reading the Report You Get
Case 3 is worth a section on its own, because of what ASan calls it:
ERROR: AddressSanitizer: heap-use-after-freeThe word "iterator" does not appear. ASan reports the mechanism — a read of freed heap memory — because that is what it observed. The freed memory is the vector's old buffer, which push_back released after copying the elements to a larger one.
So the report is accurate and the name is one level below the bug. A developer who reads heap-use-after-free and goes looking for a delete will not find one, because there is no delete in the source. The free was inside vector::push_back.
That generalises: a sanitiser names what it saw, not what you did wrong, and the gap between the two is where the diagnosis happens. The full ASan report helps here — it includes the stack of the free as well as the stack of the access, and the free's stack will show push_back, which is the sentence that identifies the bug. Chapter 25.7 §4 is the technique for reading a trace whose frames are all library code.
5. The Invalidation Rules You Have to Know
Case 3 cannot be found by a tool in general — Chapter 20.3 §5 measured an erase-in-a-loop that was silent under every tool, because the stale iterator stayed inside a live allocation below size(). So the rules themselves are load-bearing knowledge:
| operation | what it invalidates |
|---|---|
vector::push_back, insert, resize, reserve | everything, if it reallocates |
vector::erase | iterators at and after the erased position |
vector::clear | everything |
deque::push_back / push_front | all iterators; references stay valid |
list / forward_list insert | nothing |
list::erase | only the erased element's iterator |
map / set insert | nothing |
map / set erase | only the erased element's |
unordered_map insert | all iterators on rehash; references stay valid |
string append / operator+= | everything, if it reallocates |
Two things in that table are worth isolating. The node-based containers — list, map, set — invalidate almost nothing, which is a real reason to prefer them when you must hold references across mutation, and Chapter 19.4 measured what they cost to traverse. And deque and unordered_map draw a distinction between iterators and references that the others do not.
The practical rule that follows is simpler than the table: do not hold an iterator across an operation that modifies the container. Re-acquire it, or use an index, or use the erase-remove idiom, or collect what you want to change and apply it afterwards.
6. Localising the Cause, Not the Symptom
The frozen purpose asks for this specifically. When a lifetime bug surfaces, the symptom is a wrong value or a crash at the use site, and the cause is at the site where the lifetime ended — which may be in another function, another file, or inside a library.
Four moves, in the order that pays off:
Read the whole sanitiser report, not the first line. ASan prints the access stack, then the allocation stack, then the deallocation stack. The deallocation stack is the cause; the access stack is the symptom. Most of the diagnosis is in the third block, which people skip.
Ask Chapter 12's ownership question at the site that frees. Who owns this object, what is that owner's lifetime, and is there anything whose lifetime exceeds it that holds a reference? If the owner is a scope, the answer is in the braces; if it is a unique_ptr, in its member or variable; if it is a container, in the invalidation table.
Check the return types on the path. A function returning const char*, string_view, span, a reference, or an iterator is a function returning a non-owning view, and each one is a candidate for case 4. A grep for those return types in the call chain is often faster than a debugger.
Use a watchpoint when the object is overwritten rather than freed. Chapter 25.5 §5 — the watchpoint reports the instruction that wrote, which tells you which other lifetime now occupies that storage.
7. Removing the Class
Return by value, or return an owner. The first fix for case 1 and case 4 is to stop returning a view of something that is about to die:
std::string respName(); // owns its result
const char* respName(); // only safe if it points to a string literal
std::string_view respName(); // safe ONLY if the referent outlives the callerA string_view or span return is correct when the referent is a literal or something the caller already owns, and is a bug when it views a local. That distinction is not in the type, which is why Chapter 21.7's strong types and a comment naming the owner are both worth having.
Give every heap object an owner. std::unique_ptr makes case 2 almost unwriteable: there is no delete to get wrong, and the double-free and use-after-free shapes both require you to have extracted a raw pointer deliberately. Chapter 14.2 is the mechanism and Chapter 22.4 §2 measured what it prevents.
Never store a raw pointer or reference you did not establish an outlives relationship for. A member holding Device* is a claim that the Device outlives this object, and the class should say so — in a comment at minimum, and by holding a shared_ptr if the claim cannot be guaranteed.
Do not hold an iterator across a mutation. §5's rule. Where you must, use an index into a vector (stable across reallocation, though not across erase of an earlier element) or a node-based container.
Prefer the erase-remove idiom and std::erase_if over an index-and-erase loop, because they cannot be written with a stale iterator:
std::erase_if(v, [](const Txn& t) { return t.resp == Resp::Okay; }); // C++20Build the debug configuration with both detectors. -Wall -Wextra at compile time and -fsanitize=address with detect_stack_use_after_return=1 at run time, which per §3 is what covers three of the four.
8. The Table
| the question | the answer | measured |
|---|---|---|
| are these three bugs | one bug — use outside an object's lifetime | §1 |
| what ends a container element's lifetime | an operation you called on the container | §5 |
| do the plain runs report anything | no — all four silent, exit 0 | §2 |
| are the garbage values reproducible | no, unlike Chapter 25.2's | §2 |
what does -Wall -Wextra catch | the two stack cases, via -Wreturn-stack-address | §3 |
| what does ASan catch by default | the two heap cases, as heap-use-after-free | §3 |
| what catches the dangling local | ASan with detect_stack_use_after_return=1 | §3 |
what catches the dangling c_str | only the compile warning | §3 |
| does UBSan catch any | none of the four | §3 |
| what does ASan call an invalidated iterator | heap-use-after-free — the mechanism | §4 |
| where is the cause in an ASan report | the deallocation stack, the third block | §6 |
| why is coverage partial | storage class, and even string length | §6 callout |
9. What to Actually Do
- Ask Chapter 12's ownership question: who owns this, and does anything outlive the owner? That one question finds all four shapes.
- Keep
-Wall -Wextraon, because for two of the four it is the only detector — and for case 4 the only one at all. - Run ASan with
detect_stack_use_after_return=1in the debug configuration; the default omits a whole storage class. - Read the deallocation stack in an ASan report, not just the first line. The cause is in the third block.
- Return by value or return an owner. A
const char*,string_view,span, reference or iterator return is a non-owning view and needs a stated outlives relationship. - Use
unique_ptrfor every heap object, which makes case 2 nearly unwriteable. - Never hold an iterator across a container mutation; re-acquire, use an index, or use a node-based container.
- Prefer
std::erase_ifand erase-remove to index-and-erase loops. - Treat
heap-use-after-freewith nodeletein your code as an invalidated iterator until proven otherwise. - Document every raw pointer member with the lifetime claim it is making.
10. Mistakes Worth Making Once
Searching for a delete after an ASan heap-use-after-free. The free was inside push_back.
Reading only the first line of an ASan report. The deallocation stack is the cause.
Assuming ASan covers lifetime bugs. It missed both stack cases with default options.
Assuming the compiler would have warned. It said nothing about either heap case.
Returning s.c_str() from a function. Caught at compile time here, and by nothing at run time — and whether ASan could see it depends on the string's length.
Holding v.begin() across a push_back. The standard says it is invalid whether or not it reallocates this time.
Testing with short strings and shipping with long ones. Chapter 25.9 §3 measures the 22-character threshold that changes the storage class.
A raw pointer member with no stated lifetime claim. Nothing checks it and the claim is invisible.
11. Interview Relevance
The frozen curriculum marks this chapter Critical. The best opening is to decline the premise of the question: a dangling pointer, a use after free and an invalidated iterator are one bug — an object used outside its lifetime — differing only in who was responsible for ending it, which is the scope, the delete, or a method you called on the container.
The measurement to quote is the partition: -Wall -Wextra caught exactly the two stack cases via -Wreturn-stack-address, ASan with default options caught exactly the two heap cases as heap-use-after-free, UBSan caught none, and detect_stack_use_after_return=1 is what adds the dangling local. If you can say why — the compiler sees locally decidable escapes, ASan sees allocator-mediated lifetimes — you have explained the partition rather than memorised it.
The detail that lands hardest is case 4: returning s.c_str() is caught by nothing at run time, and whether ASan could even see it depends on whether the string exceeds the 22-character small-string buffer — so the detectability of one source line depends on a data value.
12. What You Should Be Able to Say Now
- That the three names describe one bug, and that the difference between them is who ended the lifetime.
- That a container operation you called is what ends an element's lifetime, and the invalidation rules for the common containers.
- That all four instances ran silently with exit 0, and that their garbage values are not reproducible — unlike Chapter 25.2's.
- The partition:
-Wall -Wextra→ the two stack cases; ASan default → the two heap cases; UBSan → none. - That
detect_stack_use_after_return=1is off by default and is what catches the dangling local. - That the dangling
c_strhas no run-time detector here, and that whether ASan could see it depends on the string's length crossing the 22-character inline buffer. - Why the partition exists: the compiler sees escapes decidable within one function; ASan sees lifetimes the allocator mediates.
- That ASan reports an invalidated iterator as
heap-use-after-free— the mechanism, not the bug — and that the deallocation stack is where the cause is. - That the remedy is ownership discipline: return values or owners,
unique_ptr, no iterators across mutations,std::erase_if.
13. Exercises
Exercise 1 — the partition. Isolate each of the four cases into its own program and reproduce the §3 table, checking exit status as well as output.
Exercise 2 — the missing option. Run case 1 under ASan with and without detect_stack_use_after_return=1. Then measure what the option costs on a larger program.
Exercise 3 — the length threshold. Make case 4's string 30 characters instead of 6 and re-run under ASan. Explain the change using the small-string buffer.
Exercise 4 — the real report. Run case 3 under ASan and read the entire report. Find push_back in the deallocation stack and explain why there is no delete in your source.
Exercise 5 — invalidation. For each row of §5's table, write a program that holds an iterator across the operation and see which configurations report it. Chapter 20.3 §5's erase-in-a-loop is the one to end on.
Exercise 6 — remove the class. Rewrite all four functions so the bug cannot be expressed: return by value, use unique_ptr, use an index, and use std::erase_if.
14. Summary
- A dangling pointer, a use after free and an invalidated iterator are one bug — an object accessed outside its lifetime — distinguished only by who ended the lifetime: a scope, a
delete, or a method you called on a container. - All four measured instances ran silently and exited 0, printing garbage that was not reproducible between runs.
- The detectors partition the class with no overlap.
-Wall -Wextracaught exactly the two stack cases, both as-Wreturn-stack-address; ASan with default options caught exactly the two heap cases, both asheap-use-after-freewith exit 134; UBSan caught none. ASAN_OPTIONS=detect_stack_use_after_return=1adds the dangling local asstack-use-after-return. It is off by default, so a plain sanitised build omits a storage class.- The dangling
c_stris caught by no run-time configuration here — only by the compile-time warning — and whether ASan could see it depends on whether the string exceeds libc++'s 22-character inline buffer. One source line, detectability decided by a data value. - The partition is structural: the compiler sees escapes that are decidable inside one function; ASan sees lifetimes the allocator mediates. Neither can cover the other's half.
- ASan names the mechanism, not the bug — an invalidated iterator is reported as
heap-use-after-free, and thefreewas insidepush_back. The deallocation stack in the report is the cause; the first line is only the symptom. - So the answer is ownership discipline, not detection: return values or owners,
unique_ptrfor every heap object, a stated outlives relationship for every raw pointer member, no iterator held across a mutation, andstd::erase_ifinstead of index-and-erase.
Chapter 25.5 turns from the bugs to the method, and replaces re-reading the source with a hypothesis and a debugger.
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.
