https://gcc.gnu.org/g:a2775feb7c7de9f21f79052e2b6a752a3eb08f07
commit r16-2511-ga2775feb7c7de9f21f79052e2b6a752a3eb08f07 Author: Iain Sandoe <i...@sandoe.co.uk> Date: Wed Jul 23 16:22:32 2025 +0100 c++, coroutines: Handle allocation fail returns [PR121219]. The current implementation was returning the result of the g_r_o_o_a_f call independently of the return expressions for 'normal' cases. This prevents the NVRO that we need to guarantee copy elision for the ramp return values - when these are initialised from a temporary of the same type. The solution here reorders the code so that the regular return expression appears before the allocation-failed case. Ensure that the g_r_o and associated code appears in a distinct scope. These steps are to meet the constaints of NRV. PR c++/121219 gcc/cp/ChangeLog: * coroutines.cc (cp_coroutine_transform::build_ramp_function): Reorder the return expressions for the 'normal' and 'allocation failed' cases so that NRV constraints are met. gcc/testsuite/ChangeLog: * g++.dg/coroutines/torture/pr121219.C: New test. Signed-off-by: Iain Sandoe <i...@sandoe.co.uk> Diff: --- gcc/cp/coroutines.cc | 39 ++++-- gcc/testsuite/g++.dg/coroutines/torture/pr121219.C | 149 +++++++++++++++++++++ 2 files changed, 174 insertions(+), 14 deletions(-) diff --git a/gcc/cp/coroutines.cc b/gcc/cp/coroutines.cc index 52cc186fbfa8..690e51007072 100644 --- a/gcc/cp/coroutines.cc +++ b/gcc/cp/coroutines.cc @@ -5050,6 +5050,8 @@ cp_coroutine_transform::build_ramp_function () check the returned pointer and call the func if it's null. Otherwise, no check, and we fail for noexcept/fno-exceptions cases. */ + tree grooaf_if_stmt = NULL_TREE; + tree alloc_ok_scope = NULL_TREE; if (grooaf) { /* [dcl.fct.def.coroutine] / 10 (part 3) @@ -5057,20 +5059,11 @@ cp_coroutine_transform::build_ramp_function () control to the caller of the coroutine and the return value is obtained by a call to T::get_return_object_on_allocation_failure(), where T is the promise type. */ - tree if_stmt = begin_if_stmt (); tree cond = build1 (CONVERT_EXPR, frame_ptr_type, nullptr_node); - cond = build2 (EQ_EXPR, boolean_type_node, coro_fp, cond); - finish_if_stmt_cond (cond, if_stmt); - r = NULL_TREE; - if (void_ramp_p) - /* Execute the get-return-object-on-alloc-fail call... */ - finish_expr_stmt (grooaf); - else - /* Get the fallback return object. */ - r = grooaf; - finish_return_stmt (r); - finish_then_clause (if_stmt); - finish_if_stmt (if_stmt); + cond = build2 (NE_EXPR, boolean_type_node, coro_fp, cond); + grooaf_if_stmt = begin_if_stmt (); + finish_if_stmt_cond (cond, grooaf_if_stmt); + alloc_ok_scope = begin_compound_stmt (BCS_NORMAL); } /* Dereference the frame pointer, to use in member access code. */ @@ -5295,7 +5288,6 @@ cp_coroutine_transform::build_ramp_function () a temp which is then used to intialize the return object, including NVRO. */ - /* Temporary var to hold the g_r_o across the function body. */ coro_gro = coro_build_and_push_artificial_var (loc, "_Coro_gro", gro_type, orig_fn_decl, NULL_TREE); @@ -5328,9 +5320,28 @@ cp_coroutine_transform::build_ramp_function () /* The ramp is done, we just need the return statement, which we build from the return object we constructed before we called the actor. */ + /* This is our 'normal' exit. */ r = void_ramp_p ? NULL_TREE : convert_from_reference (coro_gro); finish_return_stmt (r); + if (grooaf) + { + finish_compound_stmt (alloc_ok_scope); + finish_then_clause (grooaf_if_stmt); + + begin_else_clause (grooaf_if_stmt); + /* We come here if the frame allocation failed. */ + r = NULL_TREE; + if (void_ramp_p) + /* Execute the get-return-object-on-alloc-fail call... */ + finish_expr_stmt (grooaf); + else + /* Get the fallback return object. */ + r = grooaf; + finish_return_stmt (r); + finish_if_stmt (grooaf_if_stmt); + } + finish_compound_stmt (ramp_fnbody); return true; } diff --git a/gcc/testsuite/g++.dg/coroutines/torture/pr121219.C b/gcc/testsuite/g++.dg/coroutines/torture/pr121219.C new file mode 100644 index 000000000000..d1e7cb1e0225 --- /dev/null +++ b/gcc/testsuite/g++.dg/coroutines/torture/pr121219.C @@ -0,0 +1,149 @@ +// PR c++/121219 +// { dg-do run } + +#include <coroutine> +#ifdef OUTPUT +#include <iostream> +#endif +#include <stdexcept> + +struct Task { + struct promise_type; + using handle_type = std::coroutine_handle<promise_type>; + + struct promise_type { + Task* task_; + int result_; + + static void* operator new(std::size_t size) noexcept { + void* p = ::operator new(size, std::nothrow); +#ifdef OUTPUT + std::cerr << "operator new (no arg) " << size << " -> " << p << std::endl; +#endif + return p; + } + static void operator delete(void* ptr) noexcept { + return ::operator delete(ptr, std::nothrow); + } +#if 1 // change to 0 to fix crash + static Task get_return_object_on_allocation_failure() noexcept { +#ifdef OUTPUT + std::cerr << "get_return_object_on_allocation_failure" << std::endl; +#endif + return Task(nullptr); + } +#endif + + auto get_return_object() { +#ifdef OUTPUT + std::cerr << "get_return_object" << std::endl; +#endif + return Task{handle_type::from_promise(*this)}; + } + + auto initial_suspend() { +#ifdef OUTPUT + std::cerr << "initial_suspend" << std::endl; +#endif + return std::suspend_always{}; + } + + auto final_suspend() noexcept { +#ifdef OUTPUT + std::cerr << "final_suspend" << std::endl; +#endif + return std::suspend_never{}; // Coroutine auto-destructs + } + + ~promise_type() { + if (task_) { +#ifdef OUTPUT + std::cerr << "promise_type destructor: Clearing Task handle" << std::endl; +#endif + task_->h_ = nullptr; + } + } + + void unhandled_exception() { +#ifdef OUTPUT + std::cerr << "unhandled_exception" << std::endl; +#endif + std::terminate(); + } + + void return_value(int value) { +#ifdef OUTPUT + std::cerr << "return_value: " << value << std::endl; +#endif + result_ = value; + if (task_) { + task_->result_ = value; + task_->completed_ = true; + } + } + }; + + handle_type h_; + int result_; + bool completed_ = false; + + Task(handle_type h) : h_(h) { +#ifdef OUTPUT + std::cerr << "Task constructor" << std::endl; +#endif + if (h_) { + h_.promise().task_ = this; // Link promise to Task + } + } + + ~Task() { +#ifdef OUTPUT + std::cerr << "~Task destructor" << std::endl; +#endif + // Only destroy handle if still valid (coroutine not completed) + if (h_) { +#ifdef OUTPUT + std::cerr << "Destroying coroutine handle" << std::endl; +#endif + h_.destroy(); + } + } + + bool done() const { + return completed_ || !h_ || h_.done(); + } + + void resume() { +#ifdef OUTPUT + std::cerr << "Resuming task" << std::endl; +#endif + if (h_) h_.resume(); + } + + int result() const { + if (!done()) throw std::runtime_error("Result not available"); + return result_; + } +}; + +Task my_coroutine() { +#ifdef OUTPUT + std::cerr << "Inside my_coroutine" << std::endl; +#endif + co_return 42; +} + +int main() { + auto task = my_coroutine(); + while (!task.done()) { +#ifdef OUTPUT + std::cerr << "Resuming task in main" << std::endl; +#endif + task.resume(); + } +#ifdef OUTPUT + std::cerr << "Task completed in main, printing result" << std::endl; +#endif + if (task.result() != 42) + __builtin_abort (); +}