From a51ff56820f55df29fed390bd9d6a8511d6ae678 Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Thu, 3 Sep 2026 11:16:39 -0700 Subject: [PATCH 1/2] :zap: (patch) Hoist promise's future_base* owner, delete per-T cancel indirection promise_base now stores a future_base* (m_owner) instead of relying on a per-T cancel_promise static function plus an m_cancel function pointer. cancel() and unhandled_exception() only ever touch future_base's fields (m_tag, m_base), never anything T-specific, so both move from promise to promise_base as ordinary non-template member functions - one shared compiled instance instead of one per T, and no more m_cancel store emitted in every coroutine's ramp. promise_return_base::m_owner (future*) stays separate and templated, since return_value()/return_void() write into future's own m_storage, which future_base doesn't have. Measured on the usb demo (stm32f103zg, clang 20, MinSizeRel): 65,069 -> 64,821 bytes of flash (-248 B), verified via a full libhal -> libhal-util -> libhal-usb -> libhal-arm-mcu -> demos rebuild. All 12 async_context tests pass. --- modules/coroutine.cppm | 130 ++++++++++++++++++++++------------------- 1 file changed, 69 insertions(+), 61 deletions(-) diff --git a/modules/coroutine.cppm b/modules/coroutine.cppm index 3865a40..6db86be 100644 --- a/modules/coroutine.cppm +++ b/modules/coroutine.cppm @@ -1045,6 +1045,13 @@ private: // // ============================================================================= +// Forward declaration - promise_base stores a future_base* (m_owner) so that +// cancel()/unhandled_exception() can be shared, non-template functions +// instead of being re-instantiated per future. Only the pointer type is +// needed here; the bodies that dereference it are defined out-of-line below, +// after future_base is complete. +export class future_base; + /** * @brief The base promise class for coroutine operations * @@ -1259,33 +1266,38 @@ public: * @brief Cancel this coroutine operation * * This method cancels the current coroutine operation by setting its state - * to cancelled and cleaning up resources. + * to cancelled and cleaning up resources. Defined out-of-line because it + * needs future_base to be a complete type. */ - void cancel() - { - // Set future state to cancelled - m_cancel(this); - // Pop self off context stack - pop_active_coroutine(); - // Destroy promise objects & deallocate memory - std::coroutine_handle::from_promise(*this).destroy(); - } + void cancel(); -protected: /** - * @brief Type alias for cancellation function pointer + * @brief Handle unhandled exceptions in coroutines * - * This type represents the function signature used for cancellation - * callbacks. + * This method is called when a coroutine throws an exception that isn't + * handled within the coroutine itself. Only ever touches future_base's + * fields, never T, so this is one shared function for every promise + * instead of being re-instantiated per T. Defined out-of-line because it + * needs future_base to be a complete type. */ - using cancellation_fn = void(void*); + void unhandled_exception() noexcept; +protected: // Consider m_continuation as the return address of the coroutine. The // coroutine handle for the coroutine that called and awaited the future that // generated this promise is stored here. std::coroutine_handle<> m_continuation = context::noop_sentinel; class context* m_context = nullptr; - cancellation_fn* m_cancel = nullptr; + + // The future_base that owns this promise's result, set at future + // construction (and re-set on future move). Typed as future_base* - + // rather than future* - specifically so cancel() and + // unhandled_exception() above can be non-template: both only ever touch + // m_tag/m_base, which live on future_base regardless of T. This is a + // separate field from promise_return_base::m_owner below, which stays + // future*-typed because return_value()/return_void() write into + // future's own m_storage, not anything future_base has. + future_base* m_owner = nullptr; }; export template @@ -1324,6 +1336,11 @@ struct busy_state export class future_base { public: + // promise_base stores a future_base* (m_owner) and reaches into m_tag / + // m_base / state_tag directly from its (non-template, shared) + // cancel()/unhandled_exception() - see promise_base's declarations. + friend class promise_base; + using handle_type = std::coroutine_handle<>; /** @@ -1725,28 +1742,9 @@ public: return {}; } - /** - * @brief Handle unhandled exceptions in coroutines - * - * This method is called when a coroutine throws an exception that isn't - * handled within the coroutine itself. Defined out-of-line after future - * is complete. - */ - void unhandled_exception() noexcept; - - /** - * @brief Set future object associated with this promise to the - * cancelled state. - * - * This static method is used to cancel a promise by setting its owning - * future's state to cancelled. The exact promise type information is type - * erased and saved into the promise_base such that the `context` class can - * safely cancel its future objects. Defined out-of-line after future is - * complete. - * - * @param p_self Pointer to the promise to cancel - */ - static void cancel_promise(void* p_self); + // unhandled_exception() and cancel() are inherited, unmodified, from + // promise_base - neither ever needed T, so there is no per-T override + // here anymore (see promise_base's declarations above). /** * @brief Get the return object for this promise @@ -1830,9 +1828,10 @@ public: } } if (m_tag == state_tag::running) { - full_handle_type::from_address(m_base.handle.address()) - .promise() - .m_owner = this; + auto& promise = + full_handle_type::from_address(m_base.handle.address()).promise(); + promise.promise_base::m_owner = this; + promise.promise_return_base::m_owner = this; } } @@ -1864,9 +1863,10 @@ public: } } if (m_tag == state_tag::running) { - full_handle_type::from_address(m_base.handle.address()) - .promise() - .m_owner = this; + auto& promise = + full_handle_type::from_address(m_base.handle.address()).promise(); + promise.promise_base::m_owner = this; + promise.promise_return_base::m_owner = this; } } return *this; @@ -1994,8 +1994,8 @@ private: : future_base(p_handle) { auto& promise = p_handle.promise(); - promise.m_owner = this; - promise.m_cancel = &promise_type::cancel_promise; + promise.promise_base::m_owner = this; + promise.promise_return_base::m_owner = this; } union value_storage @@ -2061,29 +2061,37 @@ inline void promise_return_base::return_void() noexcept /** * @brief Handle unhandled exceptions in coroutines * - * Defined out-of-line because it needs future to be a complete type. + * Only ever touches future_base's fields (m_tag, m_base.exception), never + * anything specific to a future's T, so this is one shared, non-template + * function for every promise in the program - unlike before, when this + * lived on promise and was re-instantiated per T. Defined out-of-line + * because it needs future_base to be a complete type. */ -template -void promise::unhandled_exception() noexcept +inline void promise_base::unhandled_exception() noexcept { - auto* owner = promise_return_base::m_owner; - new (&owner->m_base.exception) std::exception_ptr(std::current_exception()); - owner->m_tag = future::state_tag::exception; + new (&m_owner->m_base.exception) std::exception_ptr(std::current_exception()); + m_owner->m_tag = future_base::state_tag::exception; } /** - * @brief Set future object associated with this promise to the cancelled - * state. + * @brief Cancel this coroutine operation * - * Defined out-of-line because it needs future to be a complete type. - * - * @param p_self Pointer to the promise to cancel + * Sets the owning future_base's state to cancelled, pops this coroutine off + * its context's active-handle stack, and destroys the coroutine frame. Only + * ever touches future_base's fields, so - like unhandled_exception() above - + * this is one shared, non-template function instead of being re-instantiated + * per future via a per-T cancel_promise() and an m_cancel function + * pointer. Defined out-of-line because it needs future_base to be a + * complete type. */ -template -void promise::cancel_promise(void* p_self) +inline void promise_base::cancel() { - auto* self = static_cast*>(p_self); - self->m_owner->m_tag = future::state_tag::cancelled; + // Set future state to cancelled + m_owner->m_tag = future_base::state_tag::cancelled; + // Pop self off context stack + pop_active_coroutine(); + // Destroy promise objects & deallocate memory + std::coroutine_handle::from_promise(*this).destroy(); } /** From 24c02d905e85346d7ac0cd2d29e30cf9e7757484 Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Thu, 3 Sep 2026 11:40:18 -0700 Subject: [PATCH 2/2] :zap: (patch) Eliminate promise_return_base's redundant m_owner pointer promise_base::m_owner (future_base*) and promise_return_base::m_owner (future*) were pointing at the same object from two separate fields, one per promise - promise_return_base is a sibling base of promise_base (both are direct bases of promise), not derived from it, so it couldn't reach promise_base's pointer without its own copy. Since promise_return_base is only ever used as a base of promise, return_value()/return_void() can reach promise_base::m_owner safely via a static_cast through promise (added as a friend of promise_base for this), then downcast to future*/future* to get at m_storage. Removes the second field (4 B/frame RAM) and the redundant second store at each of the three future construction/move call sites. Measured on the usb demo (stm32f103zg, clang 20, MinSizeRel): 64,821 -> 64,757 bytes of flash (-64 B), on top of the -248 B already landed in this PR. All 12 async_context tests pass. --- modules/coroutine.cppm | 67 ++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/modules/coroutine.cppm b/modules/coroutine.cppm index 6db86be..458f4ac 100644 --- a/modules/coroutine.cppm +++ b/modules/coroutine.cppm @@ -1052,6 +1052,15 @@ private: // after future_base is complete. export class future_base; +// Forward declaration - promise_return_base::return_value()/return_void() +// reach promise_base::m_owner (via a static_cast through promise, their +// only ever enclosing type) rather than keeping a second, redundant +// future*-typed copy of the same pointer. Needs a friend declaration +// here since promise_return_base is a sibling base of promise_base +// (both are direct bases of promise), not a derived class of it. +export template +struct promise_return_base; + /** * @brief The base promise class for coroutine operations * @@ -1065,6 +1074,8 @@ class promise_base { public: friend class context; + template + friend struct promise_return_base; // For regular functions template @@ -1291,12 +1302,12 @@ protected: // The future_base that owns this promise's result, set at future // construction (and re-set on future move). Typed as future_base* - - // rather than future* - specifically so cancel() and - // unhandled_exception() above can be non-template: both only ever touch - // m_tag/m_base, which live on future_base regardless of T. This is a - // separate field from promise_return_base::m_owner below, which stays - // future*-typed because return_value()/return_void() write into - // future's own m_storage, not anything future_base has. + // rather than future* - so that cancel() and unhandled_exception() + // above can be non-template: both only ever touch m_tag/m_base, which + // live on future_base regardless of T. promise_return_base's + // return_value()/return_void() also reach through this same pointer + // (downcasting to future*, via the friend declaration above) rather + // than keeping a second, redundant copy of it. future_base* m_owner = nullptr; }; @@ -1680,12 +1691,6 @@ struct promise_return_base template void return_value(U&& p_value) noexcept requires std::is_constructible_v; - - /** - * @brief Pointer to the future that owns this promise's result, set at - * future construction. - */ - future* m_owner = nullptr; }; /** @@ -1702,12 +1707,6 @@ struct promise_return_base * Defined out-of-line after future is complete. */ void return_void() noexcept; - - /** - * @brief Pointer to the future that owns this promise's result, set - * at future construction. - */ - future* m_owner = nullptr; }; /** @@ -1828,10 +1827,9 @@ public: } } if (m_tag == state_tag::running) { - auto& promise = - full_handle_type::from_address(m_base.handle.address()).promise(); - promise.promise_base::m_owner = this; - promise.promise_return_base::m_owner = this; + full_handle_type::from_address(m_base.handle.address()) + .promise() + .m_owner = this; } } @@ -1863,10 +1861,9 @@ public: } } if (m_tag == state_tag::running) { - auto& promise = - full_handle_type::from_address(m_base.handle.address()).promise(); - promise.promise_base::m_owner = this; - promise.promise_return_base::m_owner = this; + full_handle_type::from_address(m_base.handle.address()) + .promise() + .m_owner = this; } } return *this; @@ -1993,9 +1990,7 @@ private: explicit constexpr future(full_handle_type p_handle) : future_base(p_handle) { - auto& promise = p_handle.promise(); - promise.promise_base::m_owner = this; - promise.promise_return_base::m_owner = this; + p_handle.promise().m_owner = this; } union value_storage @@ -2029,6 +2024,9 @@ export using task = future; * @brief Handle return value for non-void futures * * Defined out-of-line because it needs future to be a complete type. + * promise_return_base is always used exclusively as a base of + * promise, so the static_cast to reach promise_base::m_owner (rather + * than keeping a second, redundant future*-typed copy of it) is safe. * * @param p_value The value to return from the coroutine */ @@ -2041,8 +2039,10 @@ void promise_return_base::return_value(U&& p_value) noexcept // assumes this pointer is uninitialized. The promise is constructed from // the future returned by `get_return_object()`, which properly initializes // this promise. - new (&m_owner->m_storage.value) T(std::forward(p_value)); - m_owner->m_tag = future::state_tag::value; + auto* owner = + static_cast*>(static_cast*>(this)->m_owner); + new (&owner->m_storage.value) T(std::forward(p_value)); + owner->m_tag = future::state_tag::value; // NOLINTEND(clang-analyzer-core.CallAndMessage) } @@ -2050,11 +2050,14 @@ void promise_return_base::return_value(U&& p_value) noexcept * @brief Handle return void for void futures * * Defined out-of-line because it needs future to be a complete type. + * Same static_cast-through-promise reasoning as return_value() above. */ inline void promise_return_base::return_void() noexcept { // NOLINTBEGIN(clang-analyzer-core.CallAndMessage) - m_owner->m_tag = future::state_tag::value; + auto* owner = + static_cast*>(static_cast*>(this)->m_owner); + owner->m_tag = future::state_tag::value; // NOLINTEND(clang-analyzer-core.CallAndMessage) }