C++ Part I · Module 12 · Constructors and Destructors
Reasoning About Lifetime in Real Code
Every example in this module has been built to isolate one fact: one resource, one exit path, one storage duration at a time. Real functions are not shaped that way. A thirty-line function will hold locals that live to the closing brace, temporaries that exist for part of a single statement and are gone before the next one starts, a reference bound to one of those temporaries whose lifetime has been quietly extended as a result, objects created and destroyed on every pass through a loop, an inner block with its own locals, and a return value that must somehow outlive the function that produced it. Each of those has been measured separately in the chapters leading here. This chapter puts all of them in one function, numbers every object, and prints every construction and destruction so the whole sequence can be read off in order — which is the skill the frozen curriculum asks for, since the ability to look at unfamiliar code and say when each object dies and what that releases is what the previous nine chapters were for. The trace also settles one thing that is easy to get wrong by assumption: the returned object is never copied, and that is not the optimiser being clever, because the measurement holds with optimisation off and with the flag whose only job is to disable such elision.
Advanced18 min readC++Part IFundamentalsLifetime
Part I · Module 12 · Chapter 12.10 · Reasoning About Lifetime in Real Code
1. The Skill
Chapter 10.5 established that lifetime is the governing idea and Chapter 12.7 tabulated when destructors run. What has not been practised is doing it on a whole function at once, which is what reading real code requires:
For every object here: when does it die, and what does that release?
This chapter is one function with six different lifetime shapes in it, fully traced.
2. The Function
Every object is numbered on construction and announces its own destruction.
struct Obj {
std::string tag;
int id;
explicit Obj(std::string t) : tag(std::move(t)), id(++seq)
{ std::printf(" +%d %s\n", id, tag.c_str()); }
Obj(const Obj& o) : tag(o.tag + " (copy)"), id(++seq)
{ ++copies; std::printf(" +%d %s\n", id, tag.c_str()); }
~Obj() { std::printf(" -%d %s\n", id, tag.c_str()); }
};
static void takesConstRef(const Obj& o) { std::printf(" using %s\n", o.tag.c_str()); }
static Obj process()
{
std::printf(" process() entered\n");
Obj local("local"); // A
takesConstRef(Obj("argument temporary")); // B
const Obj& bound = Obj("bound to const ref"); // C
std::printf(" bound.tag = %s\n", bound.tag.c_str());
for (int i = 0; i < 2; ++i) { Obj loop("loop body"); } // D
{
Obj inner("inner block"); // E
std::printf(" leaving inner block\n");
}
std::printf(" process() returning\n");
return Obj("returned value"); // F
}Called as:
{
Obj result = process();
std::printf(" got %s\n", result.tag.c_str());
std::printf(" -- leaving main's block --\n");
}(The copy constructor appends " (copy)" to the tag, so any copy is visible in the output. std::move in the first constructor is Chapter 13.9's subject and is used here only so the trace is not cluttered with string copies.)
3. The Trace
main:
process() entered
+1 local
+2 argument temporary
using argument temporary
-2 argument temporary
+3 bound to const ref
bound.tag = bound to const ref
+4 loop body
-4 loop body
+5 loop body
-5 loop body
+6 inner block
leaving inner block
-6 inner block
process() returning
+7 returned value
-3 bound to const ref
-1 local
got returned value
-- leaving main's block --
-7 returned value
main done -- copy constructor calls: 0 objects created: 7Read it once before the explanation. In particular: find where object 2 dies, and notice which objects are still alive when process() returns.
4. Object by Object
Object 1 — local. An ordinary automatic object. Destroyed at the function's closing brace, and it is the last thing destroyed inside process because it was constructed first (Chapter 12.8's reversal).
Object 2 — the argument temporary. Constructed as the argument to takesConstRef, used inside it, and destroyed immediately after the call statement — -2 appears before anything else happens. This is Chapter 12.7 §5's temporary row: a temporary dies at the end of the full expression that created it. It is valid for the duration of the call and not afterwards.
Object 3 — bound to a const reference. This is the interesting one. A temporary that would have died at the end of its statement instead survives to -3, at the end of the function. Binding a const reference directly to a temporary extends the temporary's lifetime to that of the reference — Chapter 8.8 §6 measured this and Chapter 6.5 §5 relied on it for parameters.
Note also when it dies: -3 comes before -1, because object 3 was constructed after object 1 and destruction is reverse order. The extension makes the temporary behave exactly like a local declared at that point.
Objects 4 and 5 — the loop body. Constructed and destroyed on each iteration. Two iterations, two objects, each fully destroyed before the next is created — Chapter 12.7 §5's loop row, and the reason a resource acquired in a loop body is released each time round.
Object 6 — the inner block. Destroyed at its own closing brace, before the rest of the function continues. A block is a scope.
Object 7 — the returned value. Constructed inside process as the return expression, and not destroyed when process returns — -7 appears after -- leaving main's block --. It outlived the function that created it, which is §5.
5. The Return Value Crosses the Boundary
The ordering in the trace is worth stating explicitly:
+7 returned value <-- constructed inside process()
-3 bound to const ref <-- process()'s locals destroyed
-1 local
got returned value <-- used in main
-- leaving main's block --
-7 returned value <-- destroyed in mainObject 7 is constructed before process's locals are destroyed, and destroyed after main has finished with it. So it was never inside process's frame in the sense that mattered — Chapter 10.2 §6 established that returning a pointer to a local is always wrong, and this is the contrast: a returned object is fine, because it is built where the caller can keep it.
That is the mechanism behind Chapter 6.3's advice to return by value and Chapter 10.2's callout recommending it over every alternative.
6. What Each Death Releases
The frozen description of this chapter asks for the second half too — what does each destruction release. Here each Obj holds a std::string, so:
| Object | Dies | Releases |
|---|---|---|
| 2 (argument temporary) | end of the call statement | its string's heap buffer, if any |
| 4, 5 (loop body) | each iteration | the same, twice |
| 6 (inner block) | its closing brace | the same |
3 (bound to const&) | end of the function | the same |
1 (local) | end of the function, last | the same |
| 7 (returned) | end of the caller's block | the same, in main |
Chapter 9.5 measured when a std::string actually holds a heap buffer — past the inline capacity of 22 characters. The tags here are short, so most of these releases free nothing; with longer tags each of the seven destructions would release an allocation, and Chapter 9.6 §4's allocation counter is how you would confirm it.
The general point: "what does this release" is a question about the members, which is why Chapter 12.9 §8's advice — one resource per class, compose — makes the answer easy to give. For a type holding a std::vector, a file handle and a lock, each destruction releases all three, in reverse declaration order.
7. Zero Copies, and It Is Guaranteed
The last line of the trace:
main done -- copy constructor calls: 0 objects created: 7Seven objects and no copies at all, despite return Obj(...) and Obj result = process(); which both look like they should copy. The tag of object 7 is returned value, not returned value (copy).
The obvious assumption is that the optimiser removed them. Measured:
| Build | Copy constructor calls |
|---|---|
-O0 | 0 |
-O2 | 0 |
-fno-elide-constructors | 0 |
The third row is the one that settles it. That flag's entire purpose is to disable this elision, and it cannot — because in C++17 constructing the return value directly in the caller's storage for return Obj(...) is mandatory, not an optimisation. There is no copy to elide; the standard says the object is initialized in place.
Three consequences:
Returning by value is not a performance compromise. Chapter 6.3's advice, Chapter 10.2's callout, and Chapter 9.11's Signal returned by value all cost nothing extra here.
And you cannot measure what is not there. Any reasoning of the form "I'll turn off optimisation to see the copies" fails for this case.
The full rules are Chapter 13.12's — Copy Elision and Returning Objects by Value — including the cases where elision is permitted rather than required, and what happens for a named local returned instead of a temporary. This chapter measured it because it changes the object count in the trace and would otherwise be a silent discrepancy.
8. What to Actually Do
For each object, name its storage duration and read its death off Chapter 12.7 §6's table.
Find every temporary and the end of its full expression. Anything that outlives the semicolon is a bug.
Check every reference bound to a temporary — direct bindings extend, bindings through a function call do not.
Treat a loop body as a scope — one object per iteration.
Expect a returned temporary not to be copied, mandatorily (§7).
Ask what each destruction releases, which is a question about the members.
Prefer automatic storage so that the account is short and every answer is guaranteed.
Do not try to reveal elision with -O0 or -fno-elide-constructors — measured: still zero copies.
And remember the three cases with no death to find (Chapter 12.7 §9).
9. Mistakes Worth Making Once
Keeping a pointer or reference into a temporary. It dies at the semicolon — Chapter 9.6 §5's measured dangling view.
Assuming a temporary bound to a const& dies at the end of its statement. Measured: -3 came at the end of the function.
Assuming lifetime extension reaches through a function call. It does not (Chapter 8.8 §6).
Treating a loop body as one object. Measured: objects 4 and 5, two constructions and two destructions.
Expecting a returned object to be destroyed when the function returns. Measured: -7 came in the caller.
Assuming the return copies. Measured: 0 copies, even with -fno-elide-constructors.
Trying to see the copy by lowering the optimisation level. It is not an optimisation.
Forgetting that destruction is reverse order — object 3 died before object 1.
Looking for the death of a leaked dynamic object. There is none; that is the bug.
10. Interview Relevance
"Walk me through the lifetime of every object in this function." A real interview exercise for C++ roles, usually with a function containing a temporary and a returned value. The method is what is assessed: name the storage duration, read off the death, say what it releases.
"When is a temporary destroyed?" At the end of the full expression. The follow-up that matters is the exception — bound directly to a const reference, its lifetime extends to the reference's.
"Is the returned object copied?" No, and for return T(...) it is guaranteed rather than optimised. Being able to say that -fno-elide-constructors does not change it is a strong, specific detail.
"How many objects does a loop of three iterations create?" Three, each destroyed before the next is created.
"How would you find a lifetime bug without running the code?" Account for every object by storage duration, then check whether anything holds a reference that outlives its target. The reason this matters is Chapter 10.7's measurement — a use-after-free that exits 0 with a plausible value, so testing does not find it.
11. What You Should Be Able to Say Now
- The method: name each object's storage duration, read its death off Chapter 12.7 §6's table, and say what it releases.
- Measured, one function, seven objects, six shapes, every construction and destruction printed.
- An argument temporary dies within the call statement —
-2immediately after use. - A temporary bound directly to a
const&has its lifetime extended —-3at the end of the function, not the statement. - And it dies in reverse order with the locals — object 3 before object 1 (Chapter 12.8).
- A loop body is a scope — measured: two constructions and two destructions for two iterations.
- An inner block is a scope — destroyed at its own brace.
- The returned object outlives the function —
-7came after-- leaving main's block --. - Measured: 0 copy constructor calls for 7 objects.
- Measured at
-O0,-O2and-fno-elide-constructors: still 0 — because forreturn T(...)the elision is mandatory in C++17 (Chapter 13.12). - So returning by value costs nothing — Chapter 6.3 and Chapter 10.2's advice, confirmed.
- "What does it release" is a question about the members, which is why one-resource-per-class makes it easy.
- Four things to look for: temporaries and their full expression, references bound to temporaries, loop bodies, and whether a return is the same object.
- Lifetime extension is direct-binding only — not through a function call.
- Three cases have no death to find: a leak, an incomplete construction, abnormal termination.
12. Exercises
Exercise 1 — trace it. Write the full output for a function that declares a local, calls f(Obj("t")), binds const Obj& b = Obj("b"), and returns Obj("r"), called from a block in main.
Exercise 2 — find the bug. const char* p = std::string("hello").c_str(); use(p); — say which object dies when, and why p is invalid.
Exercise 3 — extended or not? For each, say whether the temporary's lifetime is extended: (a) const Obj& r = Obj("x"); (b) const Obj& r = makeObj(); (c) const Obj& r = wrap(Obj("x")); (d) Obj&& r = Obj("x");
Exercise 4 — count them. How many objects does a four-iteration loop body create, and how many exist simultaneously?
Exercise 5 — the ordering. Explain why object 3 is destroyed before object 1 even though object 3 was a temporary.
Exercise 6 — prove the elision. Describe the experiment that shows the returned object is not copied, and say which flag you would expect to reveal a copy and what actually happens.
Exercise 7 — what is released? A type holds a std::vector<int>, a std::string and a FILE* wrapped in an RAII handle, declared in that order. Say what each destruction releases and in what order.
Exercise 8 — account for it. Take any function of your own of about thirty lines and produce the §6 table for it. Note any object you cannot confidently place.
13. Summary
- The method: storage duration → death (Chapter 12.7 §6) → what it releases.
- Measured, one function, seven objects: an argument temporary dying inside its statement, a
const&-bound temporary extended to the end of the function, two loop-body objects, an inner-block object, a local destroyed last, and a returned object destroyed in the caller. - Destruction is reverse order — object 3 before object 1.
- Measured: 0 copy constructor calls, unchanged at
-O0,-O2and under-fno-elide-constructors— the elision forreturn T(...)is mandatory (Chapter 13.12). - So returning by value is free, confirming Chapters 6.3 and 10.2.
- "What does it release" is about the members — compose one resource per class.
- Look for: temporaries and their full expression, direct bindings to temporaries, loop bodies, and whether the return is the same object.
- Lifetime extension is direct-binding only.
- Three cases have no death: a leak, an incomplete construction,
abort.
The module set out to make construction and destruction into language-run events with a guaranteed order, and that is now done — along with the technique the guarantee exists for. One question has been raised in four separate chapters and answered in none, and it is the only thing standing between the Handle class of Chapter 12.9 and a type you could actually put in a container or return from a factory. Chapter 10.8 §7 deleted its copy operations and called the deletion mandatory without saying why. Chapter 12.4 §7 explained the mechanism and said the design question was elsewhere. Chapter 12.7's callout said a hand-written destructor is a signal that raises the question immediately. Chapter 12.9 §9 named it as the chapter's one unanswered problem. The question is what it should mean to copy an object that owns something, and the next module is the answer — starting from the observation that the compiler already wrote a copy constructor for every one of these types, and that for a type with a destructor its default behaviour is precisely the double free that Chapter 10.7 measured ending in SIGTRAP.
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.
