C++ Part I · Module 7 · Scope, Storage, and Namespaces
Lifetime and Scope Are Different Questions
Chapter 7.1 was about names and said so repeatedly, deferring every question about objects to this chapter. Here is why that separation was worth insisting on. Scope is decided entirely at compile time and tells you where a name may be written; lifetime is a run-time property of an object and tells you when it exists. Those are not two descriptions of one thing, and the way to see it is to find cases where they disagree. Both exist and both are ordinary. A variable declared static inside a function keeps its value from one call to the next, which means the object is alive for the whole program — while its name is a compile error anywhere outside that function, so the object outlives any ability to refer to it by name. And in the other direction, two locals in two different functions, with different names and different values, can be measured sitting at exactly the same address, because the first stopped existing before the second began. Once both of those are on the table, the warning Chapter 6.3 met without explanation stops being a special rule to memorise. A reference to a destroyed object is simply the second case: the name is still perfectly in scope, and the object it names is gone.
Intermediate17 min readC++Part IFundamentalsScope
Part I · Module 7 · Chapter 7.2 · Lifetime and Scope Are Different Questions
1. Two Properties of Two Different Things
int cycles = 7;That one line creates an object and introduces a name for it, which is why the two get conflated. But the properties belong to different things:
| Belongs to | Decided | Answers | |
|---|---|---|---|
| Scope | the name | at compile time | where may I write this? |
| Lifetime | the object | at run time | when does this exist? |
Chapter 3.1 established the distinction this rests on: a variable is a name for an object, and an object is typed storage with a lifetime. Chapter 7.1 took the name half. This chapter takes the object half, and then shows that the two halves can come apart.
The lifetime of an object is the interval between its initialization and its destruction. Nothing about that interval is determined by where its name can be written.
2. The Two Axes Are Independent
The claim this chapter has to earn is that these are two axes rather than one. The way to earn it is to find programs in each of the two off-diagonal cases:
- An object that is alive where its name is not visible — §3.
- A name that is visible where its object is not alive — §4.
If either were impossible, the two ideas would collapse into one and Chapter 7.1 would have been enough. Both are possible, both are ordinary, and both are measured below.
A note on the measurements. This chapter argues almost entirely from addresses, so Chapter 6.2 §3's caveat applies throughout and is worth restating: the exact hex values differ on your machine and between two runs on the same machine. The claim is never a particular number — it is the relationship between numbers printed by one run.
3. An Object That Outlives Its Name
#include <iostream>
static int counter()
{
static int calls = 0; // one object, created once, never destroyed until exit
++calls;
std::cout << " calls = " << calls
<< " at address " << static_cast<const void*>(&calls) << '\n';
return calls;
}
int main()
{
counter();
counter();
counter();
// the NAME `calls` is not visible here at all -- but the object is still alive
return 0;
}Measured output:
calls = 1 at address 0x1021ac000
calls = 2 at address 0x1021ac000
calls = 3 at address 0x1021ac000Two facts in that output.
The value persisted. 1, then 2, then 3. The static int calls = 0; initializer did not run again on the second call — Chapter 7.4 is the chapter on exactly when it does run.
The address never changed. One object, in one place, for all three calls. It was not created and destroyed three times; it was created once.
Now the other half. Referring to that name from outside the function:
int main() { counter(); return calls; }error: use of undeclared identifier 'calls'Put the two together. The object exists continuously from before the first call until the program ends. Its name is writable only inside counter. So for the whole of main, there is an object that is alive and has no name you are allowed to use.
That is not a trick. It is the normal behaviour of a static local, and it is the reason Chapter 6.6 §4 could use one to count how many times a default argument was evaluated. Lifetime extends well beyond scope.
4. A Name That Outlives Its Object
Now the other direction, and it is the one that matters for bugs.
#include <iostream>
static void first()
{
int local = 11;
std::cout << " first(): local = " << local
<< " at " << static_cast<const void*>(&local) << '\n';
}
static void second()
{
int other = 22;
std::cout << " second(): other = " << other
<< " at " << static_cast<const void*>(&other) << '\n';
}
int main()
{
first();
second(); // a different object, very possibly the same storage
return 0;
}Measured output:
first(): local = 11 at 0x16d98689c
second(): other = 22 at 0x16d98689cThe same address. local and other are different names, declared in different functions, holding different values — and they occupied the same storage.
The reason is that local stopped existing when first returned. Its storage became available again, and when second was called the implementation used it for other. There is no conflict, because the two objects' lifetimes do not overlap.
One piece of storage can host many objects over the life of a program, one after another. An address does not identify an object; it identifies a location that an object may or may not currently occupy.
That sentence is the one to carry out of this chapter. It is also, said differently, why the next section's bug is a bug.
Is this guaranteed? No, and the distinction matters. That other reused local's exact address is what this implementation did, not what the standard requires — the same point Chapter 4.9 §5 made about evaluation order and Chapter 1.3 §5 made in general. What is guaranteed is that local's lifetime ended when first returned. The address reuse is evidence of that, not the rule itself.
5. All Four Combinations
With both off-diagonal cases established, the full picture:
| Name in scope | Name not in scope | |
|---|---|---|
| Object alive | an ordinary local, in use | §3 — a static local, seen from main |
| Object not alive | §6 — a reference to something destroyed | an ordinary local, after its block |
Three of those four are unremarkable. The top-right is §3's measurement and is perfectly safe — you simply cannot name the object. The bottom-right is every local variable you have ever written, after its closing brace.
The bottom-left is the one with teeth, because it is the only quadrant where you can write a name and get something wrong. In the other three, either there is nothing to say or the compiler stops you.
6. The Dangling Reference, Explained
Chapter 6.3 §6 measured this and could not fully explain it:
static int& f()
{
int local = 5;
return local; // local stops existing when f returns
}warning: reference to stack memory associated with local variable 'local' returned [-Wreturn-stack-address]Now it is a corollary rather than a rule. Line by line:
local is an object with automatic storage, whose lifetime ends when f returns — §4 established that, and measured the storage being reused afterwards.
The returned reference is a name for that object. Chapter 6.4 established that a reference is another name for an object, and §1 established that a name's scope has nothing to do with the object's lifetime.
So the caller holds a perfectly valid name for an object that no longer exists. The name is in scope. The object is gone. That is the bottom-left quadrant exactly.
What happens when you use it is undefined behaviour in Chapter 1.3 §5's precise sense — the standard places no requirements on the program. And §4 is why it is so treacherous in practice: the storage is still there, it may still contain 5, and it may be handed to the next call that needs four bytes. A program that reads it may print 5, may print something else, and may change behaviour when an unrelated function is added nearby.
This is the distinction the chapter exists for. Someone who has only one idea called "scope" has to memorise "do not return a reference to a local" as an arbitrary rule. Someone who has two axes can derive it, and can derive the cases nobody told them about.
7. Where Each Kind of Object Gets Its Lifetime
A short preview, because "when does it exist" has exactly four answers and the next chapter is about them.
| Kind of object | Lifetime runs from | until |
|---|---|---|
| a local, or a parameter | its initialization | the end of its enclosing block |
a static local | first pass through its declaration | program exit |
| a namespace-scope object | before main begins | program exit |
| one you request explicitly | when you ask for it | when you release it |
The first three appear in this chapter. The fourth is Module 10's subject and needs no mention beyond its existence.
What makes this a list of four rather than a list of cases is the idea of storage duration, which is the standard's name for the category an object belongs to. Chapter 7.3 names all four and makes the point that every object you create has exactly one of them, whether you thought about it or not.
8. What to Actually Do
Ask the two questions separately. "Can I write this name here?" and "does the object exist when this runs?" have different answers and different failure modes.
Never return a reference to a local. §6 is the derivation; the compiler warns, and it is only a warning.
Treat an address as a location, not an identity. The same address at two moments may be two different objects.
Do not keep a reference past the lifetime of what it names. Chapter 6.5 §5's temporary is the common case: safe during the call, gone after it.
Use a static local when you genuinely want an object to outlive the calls — a cache, a counter — and read Chapter 7.4 before relying on when it is initialized.
When a bug involves a value that is "sometimes right", suspect this. §4's storage reuse is exactly the mechanism that makes a dangling read work until it does not.
9. Mistakes Worth Making Once
Treating scope and lifetime as one idea. Measured both ways: an object alive with no usable name, and two names sharing one piece of storage.
Assuming an object dies when its name goes out of scope. Measured: the static local was alive for all of main, where its name was a compile error.
Assuming a name is invalid once its object is gone. The opposite — the name is fine, which is why the bug compiles.
Returning a reference to a local. -Wreturn-stack-address, and only a warning.
Reading the output of a dangling-reference program as evidence. It is undefined behaviour; the output is not a fact about C++.
Believing an address identifies an object. Measured: two objects, one address.
Expecting the address reuse in §4 to be guaranteed. The lifetime ending is guaranteed; the reuse is what this implementation did.
10. Interview Relevance
This is the module's Critical chapter and the question behind it is usually asked indirectly.
"What is wrong with returning a reference to a local variable?" The weak answer is "it dangles". The strong answer separates the two properties: the reference is a valid name, the object's lifetime ended when the function returned, and using the name is undefined behaviour. Adding that the storage is frequently reused — so the read may appear to work — explains why the bug survives testing.
"What is the difference between scope and lifetime?" Scope is a compile-time property of a name; lifetime is a run-time property of an object. If you can give an example in each direction — a static local for an object outliving its name, and storage reuse for a name outliving its object — you have demonstrated that you hold two ideas rather than one.
"Why does this bug sometimes work?" Because the storage still exists and may still hold the old value. That answer is what distinguishes someone who has debugged one.
11. What You Should Be Able to Say Now
- Scope belongs to a name and is settled at compile time; lifetime belongs to an object and is a run-time property.
- A lifetime is the interval between an object's initialization and its destruction.
- The two are independent axes, and the proof is that both off-diagonal cases exist.
- Measured — object outlives name: a
staticlocal printed 1, 2, 3 at the same address across three calls, and its name from outside waserror: use of undeclared identifier 'calls'. - Measured — name outlives object: two locals in two functions, different names and values, at the same address
0x16d98689c. - The reason is that the first object's lifetime ended when its function returned, freeing the storage.
- An address identifies a location, not an object. One location can host many objects in succession.
- The address reuse is what this implementation did; the lifetime ending is what the standard requires.
- All four quadrants: ordinary local in use;
staticlocal seen from elsewhere; a reference to a destroyed object; an ordinary local after its block. - Only the third lets you write a name and get something wrong — the other three are either safe or rejected.
- A dangling reference is that quadrant: a valid name for an object whose lifetime has ended, and using it is undefined behaviour.
- It often appears to work because the storage survives and may still hold the value — which is why §4 matters.
- Watching a lifetime end needs a destructor and is Chapter 12.7; this chapter infers it from persistence and reuse, and says so.
- Running the dangling program and reporting its output would be evidence of nothing — Chapter 5.4 §5 is the cautionary case.
12. Exercises
Exercise 1 — name the quadrant. For each, give the quadrant from §5: (a) a loop index inside its loop (b) a static local, as seen from main (c) a reference returned from a function, at the call site (d) a parameter after the function returns (e) a const& parameter during the call.
Exercise 2 — predict the output. For §3's program with a fourth counter() call added, give all four lines including what the addresses do.
Exercise 3 — explain the shared address. §4 printed one address for two objects. Say what guarantees that this is legal, and what is not guaranteed about it.
Exercise 4 — derive the rule. Without using the word "dangling", explain from §1's two definitions why int& f() { int local = 5; return local; } is a defect.
Exercise 5 — why it passes testing. A colleague's function returns a reference to a local and their tests pass. Explain, with reference to §4, why that is expected and why it is not reassuring.
Exercise 6 — separate the questions. For each statement, say whether it is about scope, lifetime, or both: (a) "calls is not declared here" (b) "calls still holds 3" (c) "the object is destroyed at the closing brace" (d) "the inner declaration hides the outer one" (e) "the storage was reused by the next call".
Exercise 7 — what you cannot see yet. Say what kind of type you would need to observe an object's destruction directly, which chapter supplies it, and why this chapter used persistence and address reuse instead.
13. Summary
- Scope is a compile-time property of a name; lifetime is a run-time property of an object.
- They are independent, and both off-diagonal cases are real.
- Measured: a
staticlocal alive at one address across three calls, with its name an error outside the function. - Measured: two locals in two functions at the same address — the first's lifetime had ended.
- An address is a location, not an identity.
- The reuse is implementation behaviour; the lifetime ending is the guarantee.
- Four quadrants, and only name in scope, object dead lets you write something wrong.
- A dangling reference is exactly that quadrant — a valid name, a destroyed object, undefined behaviour.
- It often appears to work because the storage survives — which is why it escapes testing.
- Observing destruction directly needs a destructor (12.7); this chapter says so rather than running a UB program.
This chapter answered "when does it exist" three times — for a local, for a static local, and for an object created before main — and treated those as three separate facts. They are not. The standard gives each object exactly one of four storage durations, and the answer to "when does it exist" follows from which one it has rather than from where it was written. The next chapter names all four, and the measurement that makes them concrete is a single program printing one address per category: the two static objects landed four bytes apart, the thread-local one in an entirely different region, and three nested automatic objects marched downward in even steps — one per call.
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.
