76 Advanced The Hundred
Looping Over a Corpse
two loops, one pair of parentheses apart
#include <iostream>
#include <string>
struct Widget {
std::string name_ = "the holy grail of C++ gotchas";
const std::string& name() const { return name_; }
};
Widget makeWidget() { return Widget{}; }
int main() {
std::cout << "getter: ";
for (char c : makeWidget().name())
std::cout << c;
std::cout << "\nmember: ";
for (char c : makeWidget().name_)
std::cout << c;
std::cout << '\n';
}
Run it. Do both loops print the name?
Answer
Only the second. The getter loop is undefined behavior — typical output (GCC on
x86-64) is 16 bytes of garbage that change every run, then f C++ gotchas: the tail
of a string that no longer exists. The member loop prints the name perfectly.
Why¶
Range-for desugars to auto&& __range = <your-expression>;, evaluated exactly once
before the first iteration. makeWidget().name() returns a reference into the
Widget temporary, and a reference handed back by a function call earns no lifetime
extension — the Widget dies at the end of that hidden declaration, so the loop walks
a destroyed string: undefined behavior. In the typical output the freed buffer's
first bytes have been recycled as allocator bookkeeping; with a short enough name
(SSO) it may even appear to work. The member version is the devious twin: binding a
reference directly to a member of a temporary extends the whole temporary's
lifetime to match the reference, so the second Widget outlives its entire loop.
GCC 11's -Wall -Wextra is completely silent here. C++23
(P2718) fixes the loop
itself — every temporary in the range expression now lives as long as the loop — but
GCC 11 predates it.
The fix¶
Name the owner first; a named object lives to the end of its scope:
Takeaway: never feed a range-for an expression that returns a reference into a temporary — name the owner, then loop.
Try it: g++ -std=c++17 main.cpp -o demo && ./demo