Recommended resources
The freely available resources this lesson will direct you to.
- C++ Object Lifetime: From Start to Finish - Thamara Andrade - CppCon 2023www.youtube.com
- CppCon 2018: Jason Turner “Surprises in Object Lifetime”www.youtube.com
- Lifetime - cppreference.comen.cppreference.com
- Lifetime extension of temporary objects in C++: common recommendations and pitfallspvs-studio.com
Hello! This first module builds a foundation for reasoning about ownership and lifetime in modern C++. That foundation matters whenever a function returns a reference, a view points into a buffer, or cleanup depends on leaving a scope.
In this lesson, you will learn to diagnose when an object exists, how long its storage remains available, and why a temporary sometimes survives past a semicolon—and sometimes does not. Allow about 40–45 minutes, including the selected reading and video segments.
Storage is not the object
Consider a pointer to a local variable:
int* saved = nullptr;
void remember() {
int value = 42;
saved = &value;
} // value's lifetime ends here
void use() {
int result = *saved; // undefined behavior
}
The diagnosis is not merely “the pointer points to the stack.” The precise problem is that saved still holds an address, but the int object at that address is no longer alive. Even if the bytes have not yet changed, reading through saved is invalid.
An object’s lifetime is the period during which the C++ program may treat it as that object. Its storage duration describes how long the storage associated with it lasts. These are related, but not interchangeable. An object needs appropriately sized and aligned storage; for an object that requires initialization, its lifetime begins once that initialization completes. Its lifetime can end when it is destroyed, when its storage is released, or when that storage is reused.
Read cppreference’s Lifetime page for the precise rules behind this distinction.
Read the opening of “Lifetime” to establish the start and end points of an object’s lifetime. Pay particular attention to the separate lifetime of a reference.
On the “Lifetime” page, read from the first explanatory paragraph, beginning “Every object and reference has a lifetime,” through the note about dangling references immediately above “Contents.” Follow this span closely: identify which event ends the object’s lifetime and why a reference to it can remain in scope afterward. Stop before “Temporary object lifetime.”
There are four storage-duration categories worth recognizing in code:
| Storage duration | Typical example | When the storage lasts |
|---|---|---|
| Automatic | A local variable in a block | Until its scope is exited |
| Static | A global variable or local static |
For the program’s duration |
| Thread-local | A thread_local variable |
For its thread’s duration |
| Dynamic | An object created with new |
Until the corresponding storage is released |
The category belongs to the object, not to a pointer that happens to refer to it. In auto* p = new int{42};, the pointer p is a local object with automatic storage duration; the int it points to has dynamic storage duration. Leaving p’s scope does not destroy that int. Conversely, delete p ends the pointed-to object’s lifetime even while p itself remains in scope. Dereferencing p afterward is invalid.
Thamara Andrade’s CppCon talk, C++ Object Lifetime: From Start to Finish, makes these two timelines visible through construction, scope exit, and delete.
C++ Object Lifetime: From Start to Finish - Thamara Andrade - CppCon 2023
Watch the selected opening of Thamara Andrade’s CppCon talk to connect storage-duration names with actual construction and destruction events.
Watch storage durations for the four categories, then lifetime examples through the contrast between leaving a scope and deleting a dynamically allocated object. Track the pointer and the pointed-to object separately.
A useful diagnostic habit follows: when you encounter a suspicious access, name the particular object being accessed, then identify the event that began its lifetime and the event that ended it. “The memory is still there” is not an answer to whether the object is still alive.
Temporaries and the boundary of a full-expression
Many C++ objects have no name. In the call below, make_message() produces a temporary std::string:
void inspect(const std::string& message);
inspect(make_message());
The temporary remains alive during inspect. Ordinarily, temporaries are destroyed at the end of the full-expression in which they were created—here, after the call completes, at the semicolon. The semicolon is a useful clue, though the underlying rule is the full-expression, not punctuation alone.
Read the relevant part of cppreference’s “Temporary object lifetime” section before considering its major exception.
Return to cppreference’s “Lifetime” page for the ordinary destruction rule and its reference-binding exception.
In “Temporary object lifetime,” skip the long list of situations in which temporaries are materialized. Start at the paragraph beginning “All temporary objects are destroyed as the last step” and read through the first bullet under “There are the following exceptions from that,” which introduces binding to a reference. Read the rule, then stop before the remaining exceptions and “Storage reuse.”
A reference initialized directly from a suitable temporary can extend that temporary’s lifetime to the lifetime of the reference:
const std::string& label = std::string{"bid"};
// The string remains alive while label remains in scope.
This works with an rvalue reference initialized directly from a temporary as well:
std::string&& other_label = std::string{"ask"};
// This string also remains alive while other_label remains in scope.
The critical word is directly. Lifetime extension does not travel through an arbitrary function that returns a reference:
const std::string& identity(const std::string& text) {
return text;
}
const std::string& bad = identity(std::string{"bid"});
// The temporary string dies at this semicolon.
// bad dangles on the next statement.
identity has not created a new string or extended the old string’s lifetime for its caller. It has returned another reference to the same temporary. By contrast, using that result within the same full-expression is fine:
inspect(identity(std::string{"bid"})); // Valid during inspect().
Returning a reference to an object local to the returning function has a similar outcome:
const std::string& wrong() {
std::string text = "bid";
return text;
} // text is destroyed; the returned reference dangles
Changing the caller to const auto& cannot repair either bug. The caller receives a reference to an object whose lifetime has already ended—or, in the identity example, is about to end at the current full-expression. Return an independently needed result by value instead:
std::string make_label() {
std::string text = "bid";
return text;
}
std::string label = make_label(); // label owns its string.
Returning by value does not imply that a heap allocation or an avoidable copy must occur; those are separate performance questions. More importantly, it makes the lifetime relationship clear.
Watch how Andrade applies the full-expression rule and distinguishes a reference bound directly to a temporary from a reference returned by a function.
C++ Object Lifetime: From Start to Finish - Thamara Andrade - CppCon 2023
Continue C++ Object Lifetime: From Start to Finish for the temporary-destruction rule, lifetime extension, and the return-by-reference exception.
Watch full-expressions, then reference binding. Finish with returning references. Focus on which object each reference denotes and whether binding occurs directly to a temporary or through a returned reference; you can leave the talk’s later range-loop discussion for another time.
Diagnose the referent, not the handle
References, pointers, and views can all remain available after the data they designate has ceased to be valid. This is particularly easy to miss with std::string_view: it is a lightweight non-owning description of characters, not a std::string that keeps those characters alive.
std::string_view from_local() {
std::string text = "order";
return text;
} // text is destroyed; the returned view dangles
The same issue can occur without a return statement:
std::string_view view{std::string{"order"}};
// The temporary string dies at the semicolon.
// Using view afterward to access its characters is invalid.
Compare that with:
std::string_view stable{"order"};
Here the view refers to a string literal, whose character array has static storage duration. The view still does not own anything; the difference is that its referent remains alive for the program’s duration. The characters’ origin, rather than the spelling of the view type, determines whether the access is safe.
For a further illustration, watch Jason Turner’s Surprises in Object Lifetime. His examples contrast a view of a string literal with views into local strings and arrays.
CppCon 2018: Jason Turner “Surprises in Object Lifetime”
Watch the string examples in Jason Turner’s CppCon talk to practice tracing the lifetime of the data behind a std::string_view.
In the discussion of strings, watch views and storage. Begin with the string literal’s static storage duration and continue through the local std::string and local-array cases. For each view, identify the character array it points into—not merely the view object being returned.
The same reasoning applies to a pointer retained beyond a block:
int* price = nullptr;
{
int snapshot = 101;
price = &snapshot;
} // snapshot's lifetime ends
// Reading *price here would be undefined behavior.
The supplied screenshot shows AddressSanitizer reporting this class of mistake as stack-use-after-scope: an address still exists, but the local object that occupied it has left scope.

On your Linux machine, AddressSanitizer can help expose an executed access of this kind. For a small test file, a build might use g++ -std=c++20 -O1 -g -fsanitize=address -fno-omit-frame-pointer lifetime.cpp -o lifetime. A report is evidence to investigate, not a substitute for the lifetime reasoning: a bug can exist even when a particular run produces no report.
When diagnosing a suspected lifetime bug, work in this order:
- Identify the accessed object. For a view, trace through to its underlying characters; for a pointer, trace to its pointee.
- Identify its owner or origin. Was it a local variable, a temporary, a string literal, or a dynamically allocated object?
- Mark its end-of-lifetime event. Look for scope exit, the end of a full-expression, destruction,
delete, or storage reuse. - Compare that event with the access. If the access comes later, the handle is dangling even if it still contains a plausible address.
- Choose a repair that changes the lifetime relationship. Keep the owner alive for every access, or give the recipient its own value. Merely making a reference
constor changing a pointer into a view does not create ownership.
Conclusion
The central distinction is between an object, its storage, and a handle that refers to it. Automatic, static, thread-local, and dynamic storage duration describe different storage timelines; temporaries ordinarily last through their full-expression; direct reference binding can extend a temporary’s lifetime, but returning or passing along a reference does not grant a fresh extension. To diagnose a dangling access, trace the referent’s lifetime all the way to the point of use.
In the next lesson, you will put these rules to work by implementing an RAII wrapper: a type that makes ownership and cleanup of a non-memory resource explicit.