Mircea Baja - 31 May 2025 # Callback
x
fn
--- # Capture ```cpp class callback { void* x_{ nullptr }; void (*fn_)(void* x) noexcept { nullptr }; public: callback() noexcept = default; callback(void* x, void (*fn)(void* x) noexcept) noexcept : x_{ x }, fn_{ fn } { assert(nullptr != fn_); } callback(const callback&) noexcept = default; callback& operator=(const callback&) noexcept = default; ``` - pure callback (`void`, `noexcept`, no arguments other than `x`) - some unusual copy: `default` and `noexcept`, but makes sense here --- # Invoke ```cpp void invoke() noexcept { assert(nullptr != fn_); fn_(x_); } void operator()() noexcept { invoke(); } bool is_callable() noexcept { return nullptr != fn_; } }; // callback ``` --- # Convenience, not `void` ```cpp template
struct make_function_callback_impl { static void invoke(void* x_void) noexcept { assert(x_void != nullptr); T* x = reinterpret_cast
(x_void); return std::invoke(fn, *x); } }; template
callback make_function_callback(T& x) { return callback{ &x, &make_function_callback_impl
::invoke }; } ``` - unusual order of templates in `make_function_callback` but makes sense here - unusual static in template --- # Convenience, not `void` ```cpp TEST(callback_trivial_function) { callback cb; bool called{ false }; cb = make_function_callback<+[](bool & x) noexcept { x = true; }>(called); ASSERT_FALSE(called); cb.invoke(); ASSERT_TRUE(called); } ``` - unusual lambda, uses plus, no capture => it's a function --- # Convenience, member ```cpp template
struct make_member_callback_impl { static void invoke(void* x_void) noexcept { assert(x_void != nullptr); T* x = reinterpret_cast
(x_void); return std::invoke(member_fn, x); } }; template
callback make_member_callback(T* x) { return callback{ x, &make_member_callback_impl
::invoke }; } ``` --- # Convenience, member ```cpp TEST(callback_trivial_member) { callback cb; // assuming X is a class that has a member function X x; cb = make_member_callback<&X::member_function>(&x); cb.invoke(); // x.member_function was called } ``` --- # Convenience, resume ```cpp inline callback make_resume_coroutine_callback(std::coroutine_handle<> handle) { return callback{ handle.address(), +[](void* x) noexcept { std::coroutine_handle<> original_handle = std::coroutine_handle<>::from_address(x); original_handle.resume(); }}; } ``` --- # Convenience, resume ```cpp TEST(callback_resume) { // coroutine dummy_co co = async_foo(); callback cb = make_resume_coroutine_callback(co.unique_child_coro.get()); cb.invoke(); // coroutine is resumed } ``` --- # resume - discuss resume throwing ```cpp promise_type promise(<
>); try { co_await promise.initial_suspend(); // ... body here } catch (...) { if (!made enough progress through initial_suspend) { throw; } promise.unhandled_exception(); } co_await promise.final_suspend(); ``` --- # Alternatives - std::function - D: allocation, indirect call - virtual function - D: indirect call, worse for coroutine resume or multiple member functions - resume style ABI - D: indirect call - receiver (as in senders/receivers) is a generalized callback - multiple entry points: - set_value(...), set_error(...), set_stopped() Trade offs are: - allocation - memory/space - level of call indirection --- # Applications - ready queue - timer heap - stop callback - continuations (what do do when things complete) --- # Ready queue: intrusive queue ```cpp template
class intrusive_queue { Node* head_{ nullptr }; Node* tail_{ nullptr }; public: // constructor, move etc ... bool empty() const noexcept { return head_ == nullptr; } void push(Node* what) noexcept { // add to back } Node* pop() noexcept { // return front or nullptr } }; ``` --- # Ready queue ```cpp struct ready_node { ready_node() noexcept = default; ready_node(const ready_node&) = delete; ready_node& operator=(const ready_node&) = delete; ready_node* next{}; callback cb{}; }; using ready_queue = cpp_util::intrusive_queue
; ``` --- # Ready queue: usage ```cpp // consume existing entries ready_queue local_ready = std::move(ready_queue_); while (!local_ready.empty()) { auto* ready_node = local_ready.pop(); callback cb = ready_node->cb; assert(cb.is_callable()); cb.invoke(); } // then check timer heap ``` --- # Disadvantages - not the perfect solution (we'll see other options at sender/receiver) - but will do --- # Questions?