Programming glossary 68 terms mentioned in this lesson
Select any term for a clear definition. The return button brings you back to the exact term link you used.
Lesson: Scope vs Extent (Lifetime) in C++
When you can explicitly manage memory, a big challenge is aligning when names are visible with when the data actually exists. Two questions nail it:
- Scope — Is this state (name) visible here in the source code?
- Extent (Lifetime) — Does this state (object) exist at runtime now?
Scope is a compile-time visibility rule (what identifiers you may refer to in a region of code). Extent is a runtime property (how long an object lives in memory). They are often discussed together, but they are different concepts.
Programming Insight (AI) — Draw Scope vs Extent
1) Scope: Where a Name Is Visible
- Block/lexical scope — inside
{ ... }(including function bodies). - Namespace scope — names declared at file/namespace level.
- Class scope — members inside a class/struct.
void g() {
int x = 42; // 'x' is visible only inside g() (scope)
}
// x; // ❌ out of scope (name not visible)
Scope does not say anything about memory layout or lifetime; it’s a compile-time visibility/lookup rule.
Programming Insight (AI) — Reduce Scope
2) Extent (Lifetime): When the Object Exists
- Automatic (stack) — created when its block is entered; destroyed when the block ends.
- Static storage — exists for the whole program run (
globalobjects andstaticlocals). - Thread storage — exists for the lifetime of a thread (
thread_local). - Dynamic (heap) — exists from
new/make_uniqueuntildelete/owner destruction.
// Automatic lifetime (stack)
void f() {
int a = 1; // a exists only during f()
}
// Static lifetime (for entire program)
static int s = 0;
int g = 0; // also static storage duration
Programming Insight (AI) — Pick the Right Lifetime
- Describe usage/ownership; ask AI to recommend automatic vs static vs dynamic, with pros/cons.
3) Scope ≠ Extent — They Often Diverge
A) Object outlives the name (extent > scope):
std::unique_ptr<int> make() {
auto p = std::make_unique<int>(42); // 'p' name is scoped to this function
return p; // object continues to live with the caller
} // name 'p' goes out of scope; object lifetime continues (owned by return)
B) Name remains, object is gone (scope > extent) — the dangerous case:
int* d;
{
int local = 7; // automatic lifetime ends at block end
d = &local; // ❌ d now points to an object that will soon die
} // local destroyed here
//*d = 9; // ❌ dangling pointer (name 'd' is in scope, object is dead)
Programming Insight (AI) — Find Dangling Risks
- Ask AI to mark every pointer/reference and the lifetime of the bound object; flag uses after destruction or move.
4) Static Local: Narrow Scope, Long Extent
int counter() {
static int c = 0; // scope: only inside counter(), extent: entire program
return ++c;
}
Great for memoisation/stateful helpers, but remember the object persists across calls (and threads!).
5) Heap + Pointer: Wide Extent, Limited Scope Name
void build() {
int* p = new int(5); // dynamic lifetime (until delete)
// ...
delete p; // MUST free; better: use smart pointers instead
}
The name p is scoped to build(), but the object’s lifetime is whatever you decide; mismatches cause leaks/dangling.
Programming Insight (AI) — Scope-Bound Resource Management
- Ask AI to convert raw pointers to
std::unique_ptr/std::vectorso extent ends when scope ends (RAII).
6) Reference/Pointer Bugs Caused by Lifetime Mismatch
- Return reference to a local: scope of the name at caller is fine; object is dead.
int& bad() {
int x = 42;
return x; // ❌ returns reference to destroyed object
}
- Lambda capturing a dead reference:
std::function<void()> f;
{
int v = 1;
f = [&v]{ std::cout << v << '\n'; }; // captures by reference
} // v destroyed
// f(); // ❌ dangling reference
- Thread uses reference to a local after it ends:
void tfunc(const int& r);
std::thread t;
{
int local = 5;
t = std::thread(tfunc, std::cref(local)); // uses local by reference
} // local destroyed
t.join(); // ❌ thread reads dead object
Fixes: capture/forward by value when needed; keep objects alive longer than users; or join before scope ends.
Programming Insight (AI) — Capture Audit
- Paste your lambda/thread code; ask AI to convert unsafe reference captures to value captures or extend lifetime correctly.
7) Borrowing Views: string_view, iterators, and invalidation
Non-owning types borrow another object’s storage. Their extent must not outlive the owner’s extent.
std::string_view sv;
{
std::string s = "hello";
sv = s; // sv refers to s's buffer
} // s destroyed
// sv.data(); // ❌ dangling view
Likewise, vectors can reallocate on growth; iterators/pointers become invalid.
std::vector<int> v{1,2,3};
int* p = v.data();
v.push_back(4); // may reallocate
// *p; // ❌ p may be invalid now
Programming Insight (AI) — Choose Owning/Non-Owning Types
- Ask AI to replace fragile borrows with owning copies where needed, or to add
reserve()to prevent reallocation.
8) Lifetime Extension & Moves (subtle but useful)
- Const reference binding to a temporary extends the temporary’s lifetime to the reference’s scope:
const std::string& r = std::string("hi"); // OK: temporary lives as long as 'r'
- Moves transfer resources; the moved-from object remains in scope but its invariants only are guaranteed (don’t use its old data):
std::string a = "data";
std::string b = std::move(a); // a is valid but unspecified content afterwards
Programming Insight (AI) — Spot Subtle Lifetime Rules
- Ask AI whether a reference extends a temporary’s lifetime and whether a moved-from object is safe to use in a given way.
9) Worked Examples — Align Scope with Extent
A) Return by value (tie lifetime to owner):
std::string make_name() { // value returned; lifetime owned by caller
std::string s = "Graham";
return s; // NRVO/move
}
B) Scope-bound resources with RAII:
struct File {
FILE* f{};
explicit File(const char* path): f(std::fopen(path, "r")) {}
~File(){ if (f) std::fclose(f); }
File(const File&) = delete;
File& operator=(const File&) = delete;
};
void read() { File in("data.txt"); /* use in.f */ } // closed at scope end
C) Safe lambda capture:
auto f = [name = std::string("Alice")] { std::cout << name << '\n'; };
// capture by value ensures extent covers use
Programming Insight (AI) — Generate RAII Wrappers
- Provide your resource API; ask AI to emit a small RAII wrapper (ctor acquires, dtor releases) with copy/move policy.
10) Mini Exercise — Diagnose the Mismatch
1) What’s wrong? Fix it.
const std::string& ref() {
return std::string("temp"); // ❌ returns ref to a temporary (dies at return)
}
Target: return by value std::string, or keep a static (understand consequences), or store in the caller.
2) Why can this crash?
std::string_view head_of_file() {
std::string s = load();
return std::string_view{s}.substr(0, 10); // ❌ view to destroyed string
}
Target: return std::string, or make the owner live longer, or pass the view within the same scope.
Summary Checklist
- Scope = where a name is visible; Extent = when the object exists.
- Treat them separately; align them deliberately (prefer RAII so extent ends when scope ends).
- Beware names that outlive objects (dangling) and objects that outlive names (leaks/stranded resources).
- Use value semantics & smart pointers to encode ownership; capture by value when crossing scopes/threads.
- Non-owning views/iterators are fragile: never outlive their owners; watch reallocation invalidation.
Takeaway: expert C++ design makes scope and extent obvious from types and structure — that’s how we prevent memory bugs.
Advanced perspective: visibility does not establish validity
A name in scope is not evidence that an object may still be used
Braces make the ordinary local variable look simple. Its name becomes available after the declaration, its object is created during execution, and both cease to matter when control leaves the block. From that familiar case it is tempting to make one timeline do all the work. C++ requires three.
Scope governs where a declaration can contribute a name to lookup. Storage duration classifies the broad rules under which storage is retained. Lifetime is the interval during which an object exists, with its type and usable state, in some storage. These facts often align, but correctness depends upon knowing when they do not. A compiler accepting a name proves that lookup succeeded. It does not prove that a pointer or reference reached through that name still designates a live object.
| Concept | The question it answers | Evidence it does not provide |
|---|---|---|
| Scope | Can this declaration's name be found from this source location? | That an object reached through the name is alive. |
| Storage duration | Which language rules govern how long the storage persists? | That an object currently occupies and may be used in that storage. |
| Lifetime | Has this particular object's lifetime begun, and has it not yet ended? | That a convenient name for the object is visible here. |
| Access validity | Does this pointer, reference, iterator or view still designate the intended live object or range? | Ownership, merely because an address can be followed. |
The language defines automatic, static, thread and dynamic storage durations. A call stack and a heap are common implementation mechanisms, but they are not definitions of these language properties. Saying "it is on the stack" may help to sketch an implementation. It cannot replace the reasoning needed to decide whether a particular access is valid.
Separate the declaration, the name and the object
A declaration can introduce a name and arrange for an object to be created, yet name and object are not interchangeable. Shadowing demonstrates the first separation. An inner declaration can hide an outer name while the outer object remains alive. When the inner block ends, lookup can find the outer declaration again; the outer object has not been destroyed and recreated merely because its name was hidden.
A stored pointer demonstrates the more dangerous separation. The pointer variable can remain alive and its own name can remain in scope after the pointee's lifetime has ended. We may still assign nullptr to that pointer or destroy the pointer itself. What we may not do is treat the expired address as access to the former object. The living pointer object and the dead pointee are two different lifetime claims.
| Observation | What it proves | What must still be checked |
|---|---|---|
| The identifier compiles here. | A suitable declaration participated in lookup. | Any object reached indirectly is alive and the access is permitted. |
| The address value is non-null. | The stored representation is not the null pointer value. | It designates a live object of the required kind. |
| The bytes have not visibly changed. | Only an observation about the current run and inspection. | An object lifetime still authorises reading those bytes as that object. |
| The owner remains alive. | The owner object exists. | No operation has released, replaced or invalidated the borrowed target. |
Returning a value creates a result, not a reference to an expired local
#include <iostream>
#include <string>
std::string make_label(int level) {
std::string label{"Level "};
label += std::to_string(level);
return label;
}
int main() {
const std::string label{make_label(7)};
std::cout << label << '\n';
}
Inside make_label, the local name label can be used only within its scope after its declaration. Its local object is alive until the function completes. The return statement supplies a std::string value to initialise the caller's result. Named return value optimisation may construct the result directly in its destination; when that optimisation does not apply, the language's move or copy rules still produce a value result. None of these cases gives the caller a reference to the destroyed local object.
| Stage | Name available | Live object | Permitted conclusion |
|---|---|---|---|
| While building the text | The callee's label. | The callee's local result candidate. | It may be modified inside make_label. |
| During value return | The return expression names the candidate. | A value result is produced, with permitted elision or transfer. | The interface returns a std::string, not an alias. |
| After the call | The caller's different label. | The caller's const std::string is alive. | Printing it is independent of the callee's expired local name. |
Change the return type to const std::string& and the reasoning fails. A reference value can escape the function, but the local string's lifetime still ends. The caller can possess a perfectly visible reference variable that refers to no live string. Visibility has survived; validity has not.
Every borrower carries a lifetime dependency
Raw pointers, references, iterators, std::string_view and spans can express non-owning access. Their low cost is not a lifetime guarantee. A borrower is usable only while the relevant owner keeps the target alive and while no operation invalidates the access relationship. The second condition matters because destruction is not the only invalidating event.
| Borrowed relationship | Event to inspect | Why owner existence is insufficient |
|---|---|---|
Pointer or iterator into a vector | An operation that reallocates its storage. | The vector remains alive while its elements move to a different allocation. |
string_view into a string | String destruction or a modifying operation that invalidates the viewed characters. | The view owns neither the string nor its character storage. |
| Reference captured by a callable | Invocation after the referred object has ended. | The callable object can outlive the environment from which it borrowed. |
| Reference used by another thread | The owner's destruction before the final threaded use. | A running or joinable activity does not extend an unrelated local object's lifetime. |
reserve() can sometimes make a particular sequence of vector insertions avoid reallocation, but it is not a general repair for an undocumented borrow. The required capacity, every relevant mutation and the duration of the borrow must still be known. If the relationship crosses an interface or is stored for later use, express the lifetime expectation plainly enough that a caller can satisfy it.
Temporary lifetime extension is precise, not contagious
Binding a suitable reference directly to a temporary can extend that temporary's lifetime in specified initialisation contexts. This is a rule attached to the particular binding, not a property that travels through any later reference, return statement or view. Returning a reference to a temporary created inside the function does not make the caller's reference a new extension. Constructing a non-owning view from temporary storage does not turn the view into an owner.
When reviewing such code, do not ask only whether a const reference appears. Identify the full expression that created the temporary, the exact reference initialisation, and the point at which the applicable rule ends the temporary's lifetime. If that chain cannot be shown, the reassuring keyword is doing more emotional work than technical work.
A move changes state or ownership, not the fact that the source object lives
After a valid move, the source object has not gone out of scope and its lifetime has not automatically ended. Its state is determined by the type's contract. Standard-library types are generally required to remain valid but may have an otherwise unspecified state after being moved from, unless a stronger guarantee applies. User-defined types must be judged by their own valid operations and invariants.
This distinction prevents two opposite mistakes. Assuming that the old value remains unchanged can lead to faulty use. Assuming that the object is dead can lead to skipping required destruction or denying operations that its contract permits, such as assignment of a new value. Mark the move as a state transition. Do not mark it as an imaginary closing brace.
Long lifetime does not justify wide visibility
A static local provides the useful counterexample to the idea that scope and lifetime should always be widened together. Its name remains local to the function, while the object has static storage duration and persists across calls after initialisation. This can encapsulate persistent state, but it also means that one call can affect the next. Narrow scope limits where the name can be used; it does not make the stored state transient.
Dynamic storage provides another separation. A local owning smart pointer may transfer ownership to a caller before its own name disappears. The allocated object's lifetime continues because another owner accepts the responsibility, not because the original local name somehow remains in scope. Conversely, losing the last owning handle without release can strand a resource even though no useful name remains. No name is not the same as no object.
Programming Insight (AI): demand three timelines and an invalidation ledger
Ask an AI system to draw separate lines for each declaration's scope, each object's lifetime and each relevant storage interval. Add arrows for every pointer, reference, iterator, view and captured alias. Then mark construction, move, reallocation, release and destruction at the exact program points where they occur. If the explanation uses "out of scope" for every fault, it has collapsed distinct causes into one convenient phrase.
Require the proposed repair to state what changed. Returning by value creates an owned result. Capturing by value gives the callable its own state. Joining before block exit constrains the user's operation to the owner's lifetime. Merely moving a declaration to a wider scope may silence one failure while creating unnecessary shared state. The mechanism decides whether the repair is sound.
Transfer task: prove the final use, not just the final name
Consider a function that creates a std::vector<std::string>, stores a std::string_view of its first element, moves the vector into a result object, appends another element to that result, and finally prints through the saved view. Draw the scope of each name, the lifetime of the vector and strings, and the period during which the view is valid.
You are not being asked to guess whether one run happens to print the expected characters. Identify the first operation whose specified effects might invalidate the view, and explain why the continued existence of the result vector does or does not settle the matter. Then redesign the interface in two ways: once by returning an owning string value, and once by keeping a non-owning view whose permitted lifetime is confined to a caller-controlled operation. State the cost and contract of each answer.
Reveal answer
For this trace I shall call the creating function's local vector values, its saved view firstView and its local result object result. Each name has block scope from its declaration to the function's closing brace. Moving from values does not end that scope: the name remains usable for the operations permitted on a moved-from vector. The strings are elements reached through the container rather than independent local names. If the result is returned, a caller's receiving name has a different scope and denotes the returned object; it does not extend the scope of any name in the completed call.
| Program point | Owner and object state | View judgement |
|---|---|---|
| After the local vector and its first string are created | The local vector owns a live first std::string and its character storage. | A view made from that string may inspect its current character range. |
| After ordinary allocator-preserving move construction of the result | The result owns the transferred vector storage. The moved-from vector object remains alive but no longer owns those elements. | The transfer of the vector storage does not by itself invalidate the saved access; it now reaches an element owned by the result. |
| While appending another element | The result remains alive, but the append may reallocate its element storage. | If reallocation occurs, the strings are relocated and the old view must be treated as invalid. If no reallocation occurs and the first string is not modified, the view remains valid. |
| At the final print | The result vector still exists. | Printing is permitted only if the preceding operations are known not to have invalidated the view. |
Under the ordinary move construction described above, the append is the first operation that might invalidate the view. Continued existence of the result vector does not settle the matter because an owner can remain alive while an operation relocates its elements. Capacity, the append and the representation of the contained strings are part of the evidence.
If the actual code uses an allocator-mismatched move construction or a move assignment that must move elements individually, the move itself needs a separate invalidation analysis and may be the earlier risk. The answer depends on the operation actually written; the word move is not enough.
The owning redesign copies the required text into the result before any later vector mutation:
struct BuildResult {
std::vector<std::string> values;
std::string firstValue;
};
BuildResult build_values() {
std::vector<std::string> values{"first"};
BuildResult result;
result.firstValue = values.front();
result.values = std::move(values);
result.values.emplace_back("second");
return result;
}
firstValue owns its characters, so vector reallocation and destruction do not invalidate it. The cost is a copy proportional to the number of characters, with a possible allocation. The contract is simple: the caller receives an independent string value.
The non-owning redesign finishes mutations first, then creates and consumes the view inside one caller-controlled operation:
void print_first(const std::vector<std::string>& values) {
if (values.empty()) {
return;
}
const std::string_view first{values.front()};
std::cout << first << '\n';
}
void finish_and_print(std::vector<std::string>& result) {
result.emplace_back("second");
print_first(result);
}
This version performs no character copy and normally needs no allocation for the view. Its contract is narrower: the vector and first string must remain alive and unmodified for the complete call, no concurrent operation may invalidate them, and print_first must not store the view. The caller controls that interval by completing the append before the call. Cheap borrowing is useful only because the lifetime boundary is now visible.
Scope is a rule about names. Lifetime is a rule about objects. Storage duration is a rule about storage. Safe C++ begins when those statements remain separate in the explanation and are deliberately connected in the design.