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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With