C++ Module 6 – References, pointers and memory
C++ references: lifetime extension and dangling
What a C++ reference can bind to, when binding extends a temporary's life, how returned references dangle, and the C++23 range-for fix, run on two compilers.
What you will learn
- Explain what a reference is and what each kind of reference can bind to
- Predict when binding a temporary to a reference extends its lifetime
- Find and fix dangling references with compiler warnings and AddressSanitizer
What you’ll learn
What you will learn
- Explain what a reference is and what each kind of reference can bind to
- Predict when binding a temporary to a reference extends its lifetime
- Find and fix dangling references with compiler warnings and AddressSanitizer
On this page
A reference is another name for an object that already exists. int& alias = marks; creates no new integer:
from then on alias and marks are two names for one object, and whatever you do through one name you see through
the other. A reference must be initialised when it is declared, it can never be made to refer to a different object
later, and there are no null references, because a reference may only be bound to a valid object.
A reference owns nothing, so it is only as good as the object behind it. A reference that outlives its object is
dangling, and reading through it is undefined behaviour. One rule stretches an object’s life to match a
reference, lifetime extension: a temporary bound directly to a const reference or to an rvalue reference lives
as long as that reference. This lesson shows exactly when that rule applies, the common ways references dangle, and
how compiler warnings and AddressSanitizer find them. Every program runs with Apple clang 21 and GCC 16.
A reference is another name for an object
#include <print>
int main() {
int marks = 70;
int& alias = marks; // alias is another name for marks
alias += 5;
std::println("marks {}", marks);
std::println("same object: {}", &alias == &marks);
int bonus = 10;
alias = bonus; // copies 10 into marks: alias still names marks
bonus = 99;
std::println("marks {}, bonus {}", marks, bonus);
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra alias.cpp -o alias && ./alias
g++-16 -std=c++23 -Wall -Wextra alias.cpp -o alias && ./alias
""") Output
$ clang++ -std=c++23 -Wall -Wextra alias.cpp -o alias && ./alias marks 75 same object: true marks 10, bonus 99 $ g++-16 -std=c++23 -Wall -Wextra alias.cpp -o alias && ./alias marks 75 same object: true marks 10, bonus 99
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 alias_session.py
alias += 5 changed marks, and the two addresses compare equal: one object, two names. The second assignment is
the line that surprises people. alias = bonus does not make alias refer to bonus; it copies the value 10 into
the object that alias already names, which is marks, so changing bonus afterwards changes nothing else. A
reference is bound once, when it is initialised, and it stays bound to that object until the reference itself goes
out of scope.
The standard’s section on references, [dcl.ref], adds rules that follow from “another name”: there are no references
to references, no arrays of references and no pointers to references, and a reference variable must be declared with
an initialiser. References are used most often as function parameters, const std::string& to read a large argument
without copying it and int& to let a function change the caller’s variable; the same rules apply there, with the
argument as the initialiser.
What each kind of reference can bind to
There are three kinds you will meet: T&, const T& and T&&. What each one accepts follows from what it allows you
to do with the object.
| Criterion | int& | const int& | int&& |
|---|---|---|---|
| Binds to marks | Yes | Yes | No |
| Binds to pass_mark | No | Yes | No |
| Binds to a temporary | No | Yes, and extends it | Yes, and extends it |
| Can change it | Yes | No | Yes |
| When to choose | Changing the caller's object | Reading without a copy | Moving from temporaries (later in this track) |
// What each kind of reference can bind to. The last four declarations do not compile.
int make_marks() { return 88; }
int main() {
int marks = 70;
const int pass_mark = 40;
int& a = marks; // a modifiable object
const int& b = pass_mark; // a const object
const int& c = make_marks(); // a temporary: c keeps it alive
int&& d = make_marks(); // an rvalue reference binds to temporaries only
int& e = pass_mark; // error: e could change a const int
int& f = make_marks(); // error: a temporary needs a const or an rvalue reference
int&& g = marks; // error: marks is not a temporary
int& h; // error: a reference must be initialised
} from session import run
run("""
clang++ -std=c++23 -c binding.cpp -o binding.o
g++-16 -std=c++23 -c binding.cpp -o binding.o
""", max_lines=40) Output
$ clang++ -std=c++23 -c binding.cpp -o binding.o
binding.cpp:13:10: error: binding reference of type 'int' to value of type 'const int' drops 'const' qualifier
13 | int& e = pass_mark; // error: e could change a const int
| ^ ~~~~~~~~~
binding.cpp:14:10: error: non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
14 | int& f = make_marks(); // error: a temporary needs a const or an rvalue reference
| ^ ~~~~~~~~~~~~
binding.cpp:15:11: error: rvalue reference to type 'int' cannot bind to lvalue of type 'int'
15 | int&& g = marks; // error: marks is not a temporary
| ^ ~~~~~
binding.cpp:16:10: error: declaration of reference variable 'h' requires an initializer
16 | int& h; // error: a reference must be initialised
| ^
4 errors generated.
[exit status 1]
$ g++-16 -std=c++23 -c binding.cpp -o binding.o
binding.cpp: In function 'int main()':
binding.cpp:13:14: error: binding reference of type 'int&' to 'const int' discards qualifiers
13 | int& e = pass_mark; // error: e could change a const int
| ^~~~~~~~~
binding.cpp:14:24: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
14 | int& f = make_marks(); // error: a temporary needs a const or an rvalue reference
| ~~~~~~~~~~^~
binding.cpp:15:15: error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int'
15 | int&& g = marks; // error: marks is not a temporary
| ^~~~~
binding.cpp:16:10: error: 'h' declared as reference but not initialized
16 | int& h; // error: a reference must be initialised
| ^
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 binding_session.py
Both compilers reject the same four lines in different words. A plain int& cannot bind to pass_mark, because it
would let you change a const object, and it cannot bind to the temporary that make_marks() returns: only a const
reference or an rvalue reference may bind to a temporary. An rvalue reference binds only to temporaries, so it refuses
the named variable marks. And int& h; has nothing to refer to.
When a temporary lives as long as its reference
A temporary normally lives until the end of the full-expression that created it, which in practice is the end of
the statement it appears in. Binding it to a reference changes that in one situation only: the reference must be bound
directly to the temporary. The section on temporaries, [class.temporary], lists what counts as direct: the
temporary itself, a data member of it reached with ., an element of a temporary array, and a few casts of these.
The Tracer type below prints a line when an object starts and when it ends, so the output shows each lifetime:
#include <print>
#include <string>
// Prints a line when it starts and when it ends, so the output shows the lifetime of each object.
struct Tracer {
std::string name;
explicit Tracer(std::string n) : name(std::move(n)) { std::println(" start {}", name); }
~Tracer() { std::println(" end {}", name); }
};
Tracer make(std::string name) { return Tracer(std::move(name)); }
const Tracer& same(const Tracer& t) { return t; }
int main() {
std::println("1. a const reference bound to the temporary");
{
const Tracer& kept = make("A");
std::println(" using {}", kept.name);
}
std::println("2. an rvalue reference bound to the temporary");
{
Tracer&& kept = make("B");
std::println(" using {}", kept.name);
}
std::println("3. a reference to a data member of the temporary");
{
const std::string& kept = make("C").name;
std::println(" using {}", kept);
}
std::println("4. the temporary passed through a function");
{
const Tracer& lost = same(make("D"));
std::println(" the statement that bound 'lost' has ended");
(void)lost; // reading lost here would read a destroyed object
}
} from session import run
run("""
clang++ -std=c++23 -Wall -Wextra lifetime.cpp -o lifetime && ./lifetime
g++-16 -std=c++23 -Wall -Wextra lifetime.cpp -o lifetime && ./lifetime
""") Output
$ clang++ -std=c++23 -Wall -Wextra lifetime.cpp -o lifetime && ./lifetime
1. a const reference bound to the temporary
start A
using A
end A
2. an rvalue reference bound to the temporary
start B
using B
end B
3. a reference to a data member of the temporary
start C
using C
end C
4. the temporary passed through a function
start D
end D
the statement that bound 'lost' has ended
$ g++-16 -std=c++23 -Wall -Wextra lifetime.cpp -o lifetime && ./lifetime
lifetime.cpp: In function 'int main()':
lifetime.cpp:32:23: warning: possibly dangling reference to a temporary [-Wdangling-reference]
32 | const Tracer& lost = same(make("D"));
| ^~~~
lifetime.cpp:32:39: note: 'Tracer' temporary created here
32 | const Tracer& lost = same(make("D"));
| ~~~~^~~~~
1. a const reference bound to the temporary
start A
using A
end A
2. an rvalue reference bound to the temporary
start B
using B
end B
3. a reference to a data member of the temporary
start C
using C
end C
4. the temporary passed through a function
start D
end D
the statement that bound 'lost' has ended
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 lifetime_session.py
In the first three blocks the temporary ends when its block ends, after it was used: const Tracer&, Tracer&& and a
reference to the member name all keep it alive. In the fourth block, same() takes the temporary as a parameter and
returns a reference to it. The compiler does not look inside the function to see where its result points, so nothing
is extended: D ends before the next line runs, and lost names a destroyed object. GCC’s -Wdangling-reference,
which -Wextra turns on, flags that line; Clang compiles it without a word.
When a temporary lives as long as its reference
Text description of the diagram
The diagram is a decision chart, read from top to bottom.
- It starts with a reference that is bound to a temporary object.
- The question is whether the reference is bound directly: to the temporary itself, to a data member of it reached with a dot, or to a cast of it.
- If yes, the temporary lives as long as the reference does.
- If no, the temporary reached the reference through a function, as a parameter or as a returned reference, like same() or items() in this lesson's examples. Then the temporary ends with the full-expression that created it, and the reference dangles.
- The last box names the one exception to that case: in the initializer of a range-based for loop compiled as C++23, the temporary lives until the loop ends.
The exceptions listed in [class.temporary] are all cases of a temporary reaching a reference through something else:
- A temporary bound to a reference parameter lives until the end of the full-expression that contains the call, so a function can never keep it.
- A temporary bound to the returned reference in a return statement is destroyed at the end of that statement.
- A temporary bound to a reference inside the initialiser of a new-expression lives only until the end of that full-expression.
Dangling, case 1: returning a reference to a local
The most direct way to dangle is to return a reference to a local variable. The variable is destroyed when the function returns, so the caller receives a name for an object that no longer exists:
#include <cstdio>
// Returns a reference to a local variable, which is destroyed when the function returns.
int& next_ticket() {
int ticket = 100;
++ticket;
return ticket;
}
int main() {
int& t = next_ticket();
std::printf("ticket %d\n", t);
} from session import run
# In C++20 mode the code compiles with a warning, and each build does something different.
run("""
clang++ -std=c++20 next_ticket.cpp -o next_ticket && ./next_ticket
g++-16 -std=c++20 next_ticket.cpp -o next_ticket && ./next_ticket
g++-16 -std=c++20 -S next_ticket.cpp -o - 2>/dev/null | grep -E "next_ticketv:|mov.x0, 0"
clang++ -std=c++20 -g -fsanitize=address next_ticket.cpp -o next_ticket 2>/dev/null && ./next_ticket
ASAN_OPTIONS=detect_stack_use_after_return=1 ./next_ticket
""") Output
$ clang++ -std=c++20 next_ticket.cpp -o next_ticket && ./next_ticket
next_ticket.cpp:7:12: warning: reference to stack memory associated with local variable 'ticket' returned [-Wreturn-stack-address]
7 | return ticket;
| ^~~~~~
1 warning generated.
ticket 101
$ g++-16 -std=c++20 next_ticket.cpp -o next_ticket && ./next_ticket
next_ticket.cpp: In function 'int& next_ticket()':
next_ticket.cpp:7:12: warning: reference to local variable 'ticket' returned [-Wreturn-local-addr]
7 | return ticket;
| ^~~~~~
next_ticket.cpp:5:9: note: declared here
5 | int ticket = 100;
| ^~~~~~
/bin/sh: line 1: … Segmentation fault: 11 ./next_ticket
[exit status 139]
$ g++-16 -std=c++20 -S next_ticket.cpp -o - 2>/dev/null | grep -E "next_ticketv:|mov.x0, 0"
__Z11next_ticketv:
mov x0, 0
$ clang++ -std=c++20 -g -fsanitize=address next_ticket.cpp -o next_ticket 2>/dev/null && ./next_ticket
ticket 101
$ ASAN_OPTIONS=detect_stack_use_after_return=1 ./next_ticket
==…==ERROR: AddressSanitizer: stack-use-after-return on address 0x…
READ of size 4 at 0x… thread T0
#0 in main next_ticket.cpp:12
Address 0x… is located in stack of thread T0 at offset 32 in frame
#0 in next_ticket() next_ticket.cpp:4
[32, 36) 'ticket' (line 5) <== Memory access at offset 32 is inside this variable
SUMMARY: AddressSanitizer: stack-use-after-return next_ticket.cpp:12 in main
[39 more lines of the report left out]
[exit status 134]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 next_ticket_cpp20_session.py
Both compilers warn with no warning option at all: Clang with -Wreturn-stack-address, GCC with
-Wreturn-local-addr. Then each build behaves differently, which is what undefined behaviour means in practice:
- Clang’s build printed
ticket 101, the expected number, only because nothing had overwritten the dead variable yet. Nothing promises that: another compiler version, option or call before the read may print anything. - GCC’s build crashed with a segmentation fault, exit status 139 (128 plus signal 11). The
grepline shows why: GCC compilednext_ticketto return 0, a null pointer, in place of the dead variable’s address. - The AddressSanitizer build also printed the expected number. Clang’s documentation says that the check for use
after return is switched on by default only on Linux; with
ASAN_OPTIONS=detect_stack_use_after_return=1the same program stops withstack-use-after-return, naming the variableticketfrom line 5 and the read on line 12.
Compiled as C++23, the function is no longer accepted by either compiler:
from session import run
# In C++23 mode the same function no longer compiles.
run("""
clang++ -std=c++23 next_ticket.cpp -o next_ticket
g++-16 -std=c++23 next_ticket.cpp -o next_ticket
""") Output
$ clang++ -std=c++23 next_ticket.cpp -o next_ticket
next_ticket.cpp:7:12: error: non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
7 | return ticket;
| ^~~~~~
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 next_ticket.cpp -o next_ticket
next_ticket.cpp: In function 'int& next_ticket()':
next_ticket.cpp:7:12: error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
7 | return ticket;
| ^~~~~~
[exit status 1]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 next_ticket_cpp23_session.py
C++23 adopted P2266R3, “Simpler implicit move”: in return ticket; a local variable is now treated as an object about
to be moved from (an xvalue), and a non-const int& cannot bind to that, so the dangling version stops compiling.
Clang has implemented it since Clang 13 and GCC since GCC 13. It is not a cure: a function that returns a const
reference to a local still compiles, with the same warning, as the exercise at the end of this lesson shows.
Dangling, case 2: a reference into a temporary argument
A function that returns one of its reference parameters is safe when the caller’s arguments outlive the result, and dangerous when one of them is a temporary:
#include <print>
#include <string>
// Returns whichever argument is longer: a reference to one of the caller's strings.
const std::string& longer(const std::string& a, const std::string& b) {
return a.size() >= b.size() ? a : b;
}
int main() {
std::string city = "Pune";
std::string other = "Ahmedabad";
const std::string& safe = longer(city, other); // city and other outlive safe
std::println("{}", safe);
const std::string& risky = longer(city, "Chennai"); // "Chennai" becomes a temporary string
std::println("{}", risky);
} #include <print>
#include <string>
// The same function, with each parameter marked: the result may refer to it.
const std::string& longer(const std::string& a [[clang::lifetimebound]],
const std::string& b [[clang::lifetimebound]]) {
return a.size() >= b.size() ? a : b;
}
int main() {
std::string city = "Pune";
std::string other = "Ahmedabad";
const std::string& safe = longer(city, other); // city and other outlive safe
std::println("{}", safe);
const std::string& risky = longer(city, "Chennai"); // "Chennai" becomes a temporary string
std::println("{}", risky);
} from session import run
run("""
g++-16 -std=c++23 -Wall -Wextra longer.cpp -o longer && ./longer
clang++ -std=c++23 -Wall -Wextra longer.cpp -o longer && ./longer
clang++ -std=c++23 -g -fsanitize=address longer.cpp -o longer && ./longer
clang++ -std=c++23 -Wall -Wextra -c longer_marked.cpp -o longer_marked.o
g++-16 -std=c++23 -Wall -Wextra -c longer_marked.cpp -o longer_marked.o 2>&1 | grep Wattributes
""") Output
$ g++-16 -std=c++23 -Wall -Wextra longer.cpp -o longer && ./longer
longer.cpp: In function 'int main()':
longer.cpp:15:24: warning: possibly dangling reference to a temporary [-Wdangling-reference]
15 | const std::string& risky = longer(city, "Chennai"); // "Chennai" becomes a temporary string
| ^~~~~
longer.cpp:15:45: note: 'const std::string' {aka 'const std::__cxx11::basic_string<char>'} temporary created here
15 | const std::string& risky = longer(city, "Chennai"); // "Chennai" becomes a temporary string
| ^~~~~~~~~
Ahmedabad
Chennai
$ clang++ -std=c++23 -Wall -Wextra longer.cpp -o longer && ./longer
Ahmedabad
Chennai
$ clang++ -std=c++23 -g -fsanitize=address longer.cpp -o longer && ./longer
Ahmedabad
==…==ERROR: AddressSanitizer: stack-use-after-scope on address 0x…
READ of size 1 at 0x… thread T0
#11 in main longer.cpp:16
Address 0x… is located in stack of thread T0 at offset 183 in frame
#0 in main longer.cpp:9
[160, 184) 'ref.tmp' (line 15) <== Memory access at offset 183 is inside this variable
SUMMARY: AddressSanitizer: stack-use-after-scope
[52 more lines of the report left out]
/bin/sh: line 1: … Abort trap: 6 ./longer
[exit status 134]
$ clang++ -std=c++23 -Wall -Wextra -c longer_marked.cpp -o longer_marked.o
longer_marked.cpp:16:45: warning: temporary bound to local reference 'risky' will be destroyed at the end of the full-expression [-Wdangling]
16 | const std::string& risky = longer(city, "Chennai"); // "Chennai" becomes a temporary string
| ^~~~~~~~~
1 warning generated.
$ g++-16 -std=c++23 -Wall -Wextra -c longer_marked.cpp -o longer_marked.o 2>&1 | grep Wattributes
longer_marked.cpp:5:71: warning: 'clang::lifetimebound' scoped attribute directive ignored [-Wattributes]
longer_marked.cpp:6:71: warning: 'clang::lifetimebound' scoped attribute directive ignored [-Wattributes]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 longer_session.py
The literal "Chennai" is not a std::string, so the call creates a temporary std::string for it. That temporary
is a parameter’s argument, so it is destroyed at the end of the statement, and risky may then name it. GCC warns,
again with -Wdangling-reference. Both ordinary builds printed Chennai, which looks right and is not:
AddressSanitizer stops at line 16 with stack-use-after-scope and names the temporary (ref.tmp, created on line 15).
Clang can warn here too when the function declares that its result may refer to its parameters. The attribute
[[clang::lifetimebound]] on each parameter of longer_marked.cpp does that, and Clang then reports that the
temporary bound to risky will be destroyed at the end of the full-expression. GCC does not know the attribute and says
so with -Wattributes, which is harmless, but in code shared between compilers the attribute is usually hidden behind
a macro.
Dangling, case 3: a range-for loop over a temporary, fixed in C++23
A range-based for loop keeps the result of its range expression in a hidden reference, and loops over that. When the
expression calls a member function on a temporary, the member function returns a reference into the temporary, and
the temporary itself is not extended:
#include <iostream>
#include <vector>
struct Batch {
std::vector<int> marks;
const std::vector<int>& items() const { return marks; }
};
Batch make_batch() { return Batch{{72, 85, 91}}; }
int main() {
int total = 0;
for (int m : make_batch().items()) total += m; // the loop runs over a member of a temporary
std::cout << "total " << total << '\n';
} from session import run
run("""
clang++ -std=c++20 -g -fsanitize=address batch_loop.cpp -o batch_loop && ./batch_loop
g++-16 -std=c++20 -Wall -Wextra -g -fsanitize=address batch_loop.cpp -o batch_loop && ./batch_loop
clang++ -std=c++23 -g -fsanitize=address batch_loop.cpp -o batch_loop && ./batch_loop
g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address batch_loop.cpp -o batch_loop && ./batch_loop
""") Output
$ clang++ -std=c++20 -g -fsanitize=address batch_loop.cpp -o batch_loop && ./batch_loop
==…==ERROR: AddressSanitizer: stack-use-after-scope on address 0x…
READ of size 8 at 0x… thread T0
#1 in main batch_loop.cpp:13
Address 0x… is located in stack of thread T0 at offset 32 in frame
#0 in main batch_loop.cpp:11
[32, 56) 'ref.tmp' (line 13) <== Memory access at offset 32 is inside this variable
SUMMARY: AddressSanitizer: stack-use-after-scope
[42 more lines of the report left out]
/bin/sh: line 1: … Abort trap: 6 ./batch_loop
[exit status 134]
$ g++-16 -std=c++20 -Wall -Wextra -g -fsanitize=address batch_loop.cpp -o batch_loop && ./batch_loop
batch_loop.cpp: In function 'int main()':
batch_loop.cpp:13:37: warning: possibly dangling reference to a temporary [-Wdangling-reference]
13 | for (int m : make_batch().items()) total += m; // the loop runs over a member of a temporary
| ^
batch_loop.cpp:13:28: note: 'Batch' temporary created here
13 | for (int m : make_batch().items()) total += m; // the loop runs over a member of a temporary
| ~~~~~~~~~~^~
==…==ERROR: AddressSanitizer: stack-use-after-scope on address 0x…
READ of size 8 at 0x… thread T0
#1 in main batch_loop.cpp:13
Address 0x… is located in stack of thread T0 at offset 96 in frame
#0 in main batch_loop.cpp:11
[96, 120) '<unknown>' <== Memory access at offset 96 is inside this variable
SUMMARY: AddressSanitizer: stack-use-after-scope
[42 more lines of the report left out]
/bin/sh: line 1: … Abort trap: 6 ./batch_loop
[exit status 134]
$ clang++ -std=c++23 -g -fsanitize=address batch_loop.cpp -o batch_loop && ./batch_loop
total 248
$ g++-16 -std=c++23 -Wall -Wextra -g -fsanitize=address batch_loop.cpp -o batch_loop && ./batch_loop
total 248
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 batch_loop_session.py
In C++20 mode, the Batch returned by make_batch() is destroyed as soon as the hidden reference is initialised, so
the loop walks a destroyed vector, and both sanitizer builds stop at line 13. C++23 changed the rule with P2718R0: every
temporary created in the range expression of a range-based for, apart from function parameters passed by value, now
lives until the loop ends. The same source compiled as C++23 prints total 248 with both compilers. Support arrived in
Clang 20 and GCC 15, and code can check that the feature-test macro __cpp_range_based_for is at least 202211L.
Two cautions. The fix applies only in C++23 mode, so the same file still dangles when someone builds it as C++20. And
it covers only the range expression of a for loop: const auto& v = make_batch().items(); on its own line dangles
in every version.
Reference members, and std::reference_wrapper
A reference can be a data member, but it makes the class awkward. A reference cannot be re-seated, so the compiler cannot write the usual copy assignment, and [class.copy.assign] defines it as deleted:
#include <string>
struct Ticket {
const std::string& passenger; // a reference member
int seat;
};
int main() {
std::string asha = "Asha";
std::string ravi = "Ravi";
Ticket a{asha, 12};
Ticket b{ravi, 14};
a = b; // error: Ticket cannot be assigned
} #include <functional>
#include <print>
#include <string>
struct Ticket {
std::reference_wrapper<const std::string> passenger; // refers to a string, and can be re-pointed
int seat;
};
int main() {
std::string asha = "Asha";
std::string ravi = "Ravi";
Ticket a{asha, 12};
Ticket b{ravi, 14};
a = b; // a now refers to ravi
std::println("{} {}", a.passenger.get(), a.seat);
ravi += " Kumar"; // a still refers to the same string, not a copy
std::println("{} {}", a.passenger.get(), a.seat);
} from session import run
run("""
clang++ -std=c++23 -c ticket_reference.cpp -o ticket_reference.o
g++-16 -std=c++23 -c ticket_reference.cpp -o ticket_reference.o
clang++ -std=c++23 -Wall -Wextra ticket_wrapper.cpp -o ticket_wrapper && ./ticket_wrapper
""") Output
$ clang++ -std=c++23 -c ticket_reference.cpp -o ticket_reference.o
ticket_reference.cpp:13:7: error: object of type 'Ticket' cannot be assigned because its copy assignment operator is implicitly deleted
13 | a = b; // error: Ticket cannot be assigned
| ^
ticket_reference.cpp:4:24: note: copy assignment operator of 'Ticket' is implicitly deleted because field 'passenger' is of reference type 'const std::string &' (aka 'const basic_string<char> &')
4 | const std::string& passenger; // a reference member
| ^
1 error generated.
[exit status 1]
$ g++-16 -std=c++23 -c ticket_reference.cpp -o ticket_reference.o
ticket_reference.cpp: In function 'int main()':
ticket_reference.cpp:13:9: error: use of deleted function 'Ticket& Ticket::operator=(const Ticket&)'
13 | a = b; // error: Ticket cannot be assigned
| ^
ticket_reference.cpp:3:8: note: 'Ticket& Ticket::operator=(const Ticket&)' is implicitly deleted because the default definition would be ill-formed:
3 | struct Ticket {
| ^~~~~~
ticket_reference.cpp: At global scope:
ticket_reference.cpp:3:8: error: non-static reference member 'const std::string& Ticket::passenger', cannot use default assignment operator
ticket_reference.cpp: In function 'int main()':
ticket_reference.cpp:13:9: note: use '-fdiagnostics-all-candidates' to display considered candidates
13 | a = b; // error: Ticket cannot be assigned
| ^
[exit status 1]
$ clang++ -std=c++23 -Wall -Wextra ticket_wrapper.cpp -o ticket_wrapper && ./ticket_wrapper
Ravi 14
Ravi Kumar 14
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 ticket_session.py
Such objects can still be put into a std::vector, but the vector cannot be sorted or have an element erased from the
middle, because the standard requires assignable elements for both. That is why the C++ Core Guidelines say, in C.12,
not to make data members references in a copyable or movable type. When a member must refer to an object owned
elsewhere and must also be re-pointed, use std::reference_wrapper<T> from <functional> as ticket_wrapper.cpp
does: assignment re-points it, and .get() returns the reference. A pointer works too and
additionally allows “no object” (the next lesson). Neither one changes who owns the string: if ravi were destroyed
first, a.passenger would dangle exactly as a reference would.
Your turn: fix four dangling references
Each function in this program hands back or keeps a reference to an object that dies too early. Fix all four so that
the program prints Hello, Asha, Asha, Results and 91 on four lines and AddressSanitizer reports nothing:
// Four dangling references. Fix each function so the program prints
// Hello, Asha / Asha / Results / 91
// on four lines and AddressSanitizer reports nothing.
#include <print>
#include <string>
#include <vector>
// 1. Builds a greeting for a name.
const std::string& greeting(const std::string& name) {
std::string text = "Hello, " + name;
return text;
}
// 2. Makes a class list on the spot.
std::vector<std::string> class_list() { return {"Asha", "Ravi", "Meera"}; }
// 3. A label that shows a title.
struct Label {
const std::string& title;
};
Label results_label() {
std::string title = "Results";
return Label{title};
}
// 4. The highest of some marks.
const int& highest(const std::vector<int>& marks) {
const int* best = &marks[0];
for (const int& m : marks)
if (m > *best) best = &m;
return *best;
}
int main() {
const std::string& hello = greeting("Asha");
std::println("{}", hello);
const std::string& first = class_list().front();
std::println("{}", first);
Label label = results_label();
std::println("{}", label.title);
const int& top = highest({72, 91, 85});
std::println("{}", top);
} As given, Clang warns about two of the bugs and GCC about three, but neither sees the reference member in Label, and
the first sanitizer report stops at greeting:
from session import run
# The exercise as given: what the compilers warn about, then the first report of AddressSanitizer.
run("""
clang++ -std=c++23 -Wall -Wextra -c dangling_exercise.cpp -o dangling_exercise.o
g++-16 -std=c++23 -Wall -Wextra -c dangling_exercise.cpp -o dangling_exercise.o
clang++ -std=c++23 -g -fsanitize=address dangling_exercise.cpp -o dangling_exercise 2>/dev/null
ASAN_OPTIONS=detect_stack_use_after_return=1 ./dangling_exercise
""", max_lines=40) Output
$ clang++ -std=c++23 -Wall -Wextra -c dangling_exercise.cpp -o dangling_exercise.o
dangling_exercise.cpp:11:12: warning: reference to stack memory associated with local variable 'text' returned [-Wreturn-stack-address]
11 | return text;
| ^~~~
dangling_exercise.cpp:39:32: warning: object backing the pointer will be destroyed at the end of the full-expression [-Wdangling-gsl]
39 | const std::string& first = class_list().front();
| ^~~~~~~~~~~~
2 warnings generated.
$ g++-16 -std=c++23 -Wall -Wextra -c dangling_exercise.cpp -o dangling_exercise.o
dangling_exercise.cpp: In function 'const std::string& greeting(const std::string&)':
dangling_exercise.cpp:11:12: warning: reference to local variable 'text' returned [-Wreturn-local-addr]
11 | return text;
| ^~~~
dangling_exercise.cpp:10:17: note: declared here
10 | std::string text = "Hello, " + name;
| ^~~~
dangling_exercise.cpp: In function 'int main()':
dangling_exercise.cpp:36:24: warning: possibly dangling reference to a temporary [-Wdangling-reference]
36 | const std::string& hello = greeting("Asha");
| ^~~~~
dangling_exercise.cpp:36:41: note: 'const std::string' {aka 'const std::__cxx11::basic_string<char>'} temporary created here
36 | const std::string& hello = greeting("Asha");
| ^~~~~~
dangling_exercise.cpp:39:24: warning: possibly dangling reference to a temporary [-Wdangling-reference]
39 | const std::string& first = class_list().front();
| ^~~~~
dangling_exercise.cpp:39:42: note: 'std::vector<std::__cxx11::basic_string<char> >' temporary created here
39 | const std::string& first = class_list().front();
| ~~~~~~~~~~^~
dangling_exercise.cpp:45:16: warning: possibly dangling reference to a temporary [-Wdangling-reference]
45 | const int& top = highest({72, 91, 85});
| ^~~
dangling_exercise.cpp:45:29: note: 'const std::vector<int>' temporary created here
45 | const int& top = highest({72, 91, 85});
| ~~~~~~~^~~~~~~~~~~~~~
$ clang++ -std=c++23 -g -fsanitize=address dangling_exercise.cpp -o dangling_exercise 2>/dev/null
$ ASAN_OPTIONS=detect_stack_use_after_return=1 ./dangling_exercise
==…==ERROR: AddressSanitizer: stack-use-after-return on address 0x…
READ of size 1 at 0x… thread T0
#11 in main dangling_exercise.cpp:37
Address 0x… is located in stack of thread T0 at offset 55 in frame
#0 in greeting(std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char>> const&) dangling_exercise.cpp:9
[32, 56) 'text' (line 10) <== Memory access at offset 55 is inside this variable
SUMMARY: AddressSanitizer: stack-use-after-return
[50 more lines of the report left out]
[exit status 134]
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dangling_exercise_session.py
- Fix the bug the report names, rebuild with the AddressSanitizer command and run it again. AddressSanitizer stops at
the first error, so each run shows the next bug. On Linux,
detect_stack_use_after_returnis already switched on. - Repeat until the program prints its four lines and nothing else.
- Build with
-Wall -Wextraon both compilers once more: a good fix also leaves no warning.
First hint: for each report, ask which object the reference names and when that object ends. If it ends before the reference is used, either keep the object alive for longer, in a named variable, or stop referring to it and keep a value instead.
Self-check
Check yourself
5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.
Interview questions
Fresher · Can a reference be null, and can it be made to refer to another object?
Neither. A reference must be initialised to refer to a valid object, and it stays bound to that object for its whole
life: assigning to a reference assigns to the object it names, so r = other; copies other’s value and does not
re-point r. A null reference cannot exist in a valid program, because the only way to make one is to dereference a
null pointer, which is already undefined behaviour. A reference can still dangle, though, when the object it names is
destroyed first, as a reference returned from a function to its local variable does. When you need “no object” or
re-pointing, use a pointer, std::optional<std::reference_wrapper<T>> or std::reference_wrapper<T> instead.
Key takeaways
- A reference is another name for an existing object: it must be initialised, it cannot be re-seated, and assigning through it changes the object it names.
T&binds only to modifiable objects;const T&binds to anything, temporaries included;T&&binds only to temporaries.- A temporary bound directly to a
const T&orT&&lives as long as the reference; bound through a function, as a parameter or a returned reference, it dies at the end of the full-expression. - Returning a reference to a local dangles. Compilers warn by default, and C++23 rejects it outright for non-const references; in C++20 mode the runs gave a correct-looking number, a crash, and a report only with the right option.
- C++23 keeps the temporaries of a range-for’s range expression alive until the loop ends (Clang 20, GCC 15); C++20
code that loops over
make().items()reads a destroyed object. - Warnings catch some dangling references and AddressSanitizer catches more; on macOS, add
ASAN_OPTIONS=detect_stack_use_after_return=1to find references returned to dead locals.
References
- Working Draft, Programming Languages: C++ (N4950), [dcl.ref], [class.temporary], [class.copy.assign] (ISO/IEC JTC1/SC22/WG21)
- P2718R0: Wording for P2644R1 Fix for Range-based for Loop (ISO/IEC JTC1/SC22/WG21)
- P2266R3: Simpler implicit move (ISO/IEC JTC1/SC22/WG21)
- AddressSanitizer (Clang documentation) (LLVM Project)
- Attributes in Clang (lifetimebound) (LLVM Project)
- Options Controlling C++ Dialect (-Wdangling-reference) (GNU Project)
- Options to Request or Suppress Warnings (-Wreturn-local-addr) (GNU Project)
- C++ Support in Clang (LLVM Project)
- C++ Standards Support in GCC (GNU Project)
- C++ Core Guidelines (F.43, C.12) (Standard C++ Foundation)
Report a problem with this lesson
Kept only in this browser. Your Learn progress