C++23 was supposed to fix issues around range-based for
loops. The C++ standard now has wording that I can trace to p2718r0, including the following example at the end of the section "The range-based for statement [stmt.ranged]
" (note the undefined behavior
comment:
using T = std::list<int>;
const T& f1(const T& t) { return t; }
const T& f2(T t) { return t; }
T g();
void foo() {
for (auto e : f1(g())) {} // OK, lifetime of return value of g() extended
for (auto e : f2(g())) {} // undefined behavior
}
Let's say I write a coroutine using the C++23 std::generator
that takes an argument by value i.e. following C++ core guidelines rule CP.53: Parameters to coroutines should not be passed by reference:
#include <generator>
#include <string>
#include <iostream>
std::generator<std::string> generator_f(std::string x) {
co_yield x;
}
std::string sample_g() { return "abc"; }
int main() {
for(const auto& elem: generator_f(sample_g())) {
std::cout << elem << "\n";
}
return 0;
}
I assume that based on the example in the C++ standard, the code in the for
loop in main()
above is undefined behavior.
What's the rationale behind the undefined behavior rule illustrated by the example in the C++ standard? The downside is that it makes error prone the usage of a std::generator
with arguments in a for
loop (which is where you would want to use a generator, i.e. not really fixing lifetime problems with range-based for
statements).
The undefined behavior in this code:
for (auto e : f2(g())) {} // undefined behavior
comes from f2()
itself:
const T& f2(T t) { return t; }
This function is returning a reference to its parameter t
, which is destroyed at the end of the function call. That's not at all tied in to the expression f2(g())
.
The difference in this code:
for (auto e : f1(g())) {} // OK
is that f1()
returns a reference to the temporary object returned by g()
, which lasts until the end of the full-expression that creates it. But previously, that full-expression was the declaration of the compiler-generated range variable, not the actual loop. The change for C++23 is that now the lifetime of that temporary is extended through the whole loop.
That's not the situation with f2()
at all. f2()
isn't returning a reference to the temporary object returned by g()
, it is returning a reference to its own parameter t
. Extending the lifetime of the temporary returned by g()
through the whole loop doesn't fix the problem.
On the other hand, this code:
int main() {
for(const auto& elem: generator_f(sample_g())) {
std::cout << elem << "\n";
}
return 0;
}
has always been fine. Even before P2718. There's not even any references binding to temporaries in this example.