xo-umbrella2/xo-callback2/include/xo/callback2/CallbackSet.hpp

330 lines
12 KiB
C++

/** @file CallbackSet.hpp
*
* Arena-allocated set of callbacks, with explicit obj<AAllocator>.
*
* @author Roland Conybeare, Aug 2026
**/
#pragma once
#include "callback_traits.hpp"
#include <xo/callback/CallbackId.hpp>
#include <xo/alloc2/Allocator.hpp>
#include <xo/facet/obj.hpp>
#include <cstdint>
#include <new>
namespace xo {
namespace fn {
/** one registered callback **/
template <typename Callback>
struct CbRecd {
CbRecd() = default;
CbRecd(CallbackId id, Callback fn) : id_{id}, fn_{fn} {}
/** identifies this callback, for remove_callback() **/
CallbackId id_;
/** the callback itself **/
Callback fn_;
/** for a slot in the deferred region: true iff a deferred add.
* unused for live slots.
**/
bool is_add_ = false;
}; /*CbRecd*/
/** @class CallbackSet
* @brief set of callbacks, allocated from an explicit obj<AAllocator>
*
* @tp Callback either a faceted obj<AFacet,DRepr>
* or a plain function pointer.
* std::function and lambdas rejected, because for faceted
* objects we need to support AGCObject.
*
* Storage follows the DArray idiom: capacity is fixed at allocation,
* add_callback() fails when full, and grow() is explicit via copy().
* There is no reallocation, so nothing is ever stranded in the arena.
*
* Reentrant: callbacks running under invoke()/invoke_each() may add or
* remove callbacks on this set; such changes are deferred to the end of
* the run. The deferral region has its own fixed capacity.
*
* Not threadsafe.
**/
template <typename Callback>
class CallbackSet {
public:
using size_type = std::uint32_t;
using callback_type = Callback;
using slot_type = CbRecd<Callback>;
static_assert(!is_std_function_v<Callback>,
"xo::fn::CallbackSet: std::function is not supported."
" It allocates from the heap (its allocator-extended"
" ctors were removed in c++17), and hides captured"
" references from the collector."
" Use obj<AFacet> or a plain function pointer.");
static_assert(callback2_concept<Callback>,
"xo::fn::CallbackSet: unsupported callback type."
" Expected obj<AFacet,DRepr> (traced) or a plain"
" function pointer (untraced). Closure types"
" (lambdas) are excluded: they can capture an"
" obj<AFacet> that the collector cannot see."
" Pass a lambda to invoke_each() instead -- that one"
" is transient, not stored.");
/** true iff callbacks in this set can hold traced references **/
static constexpr bool is_traced = fomo_callback_concept<Callback>;
public:
/** create empty set with room for @p cap callbacks and @p defer_cap
* deferred add/remove commands, using memory from @p mm.
* Nullptr if space exhausted.
**/
static CallbackSet * _empty(xo::facet::obj<xo::mm::AAllocator> mm,
size_type cap,
size_type defer_cap)
{
CallbackSet * result = nullptr;
void * mem
= mm.alloc(xo::reflect::typeseq::id<CallbackSet>(),
(sizeof(CallbackSet)
+ (cap + defer_cap) * sizeof(slot_type)));
if (mem) [[likely]] {
result = new (mem) CallbackSet();
result->capacity_ = cap;
result->defer_capacity_ = defer_cap;
}
return result;
}
/** create empty set, deferral capacity defaulting to @p cap **/
static CallbackSet * _empty(xo::facet::obj<xo::mm::AAllocator> mm,
size_type cap)
{
return _empty(mm, cap, cap);
}
/** copy of @p src with capacity for @p new_cap callbacks.
* This is how a set grows. Nullptr if space exhausted.
*
* Require: src has no deferred commands pending (i.e. is not
* running), and new_cap >= src->size()
**/
static CallbackSet * copy(xo::facet::obj<xo::mm::AAllocator> mm,
const CallbackSet * src,
size_type new_cap)
{
if (!src || (new_cap < src->size_))
return nullptr;
CallbackSet * result = _empty(mm, new_cap,
src->defer_capacity_);
if (result) [[likely]] {
for (size_type i = 0; i < src->size_; ++i)
result->slot_[i] = src->slot_[i];
result->size_ = src->size_;
}
return result;
}
/** @defgroup cbset-access access **/
///@{
bool is_empty() const noexcept { return size_ == 0; }
size_type capacity() const noexcept { return capacity_; }
size_type size() const noexcept { return size_; }
bool is_full() const noexcept { return size_ >= capacity_; }
/** true while callbacks from this set are running **/
bool is_running() const noexcept { return running_; }
const slot_type * begin() const noexcept { return slot_; }
const slot_type * end() const noexcept { return slot_ + size_; }
///@}
/** @defgroup cbset-modify add/remove **/
///@{
/** register @p fn. Returns its id, or a default-constructed
* CallbackId if the set (or, while running, the deferral region)
* is full.
*
* Reentrant: safe to call from a running callback.
**/
CallbackId add_callback(Callback fn) noexcept {
CallbackId id = CallbackId::generate();
if (running_) {
if (n_defer_ >= defer_capacity_)
return CallbackId();
slot_type & s = slot_[capacity_ + n_defer_];
s = slot_type(id, fn);
s.is_add_ = true;
++n_defer_;
} else {
if (this->is_full())
return CallbackId();
slot_[size_] = slot_type(id, fn);
++size_;
}
return id;
}
/** unregister the callback with id @p id. No-op if absent.
* Reentrant.
**/
bool remove_callback(CallbackId id) noexcept {
if (running_) {
if (n_defer_ >= defer_capacity_)
return false;
slot_type & s = slot_[capacity_ + n_defer_];
s = slot_type(id, Callback());
s.is_add_ = false;
++n_defer_;
return true;
}
return this->remove_callback_impl(id);
}
///@}
/** @defgroup cbset-invoke invocation **/
///@{
/** call fn(cb, args...) for each registered callback cb.
*
* @p fn is not stored, so a lambda is fine here
* lambda is fine here, and is the idiomatic way to name a facet
* method:
*
* cbset.invoke_each([](auto & cb, const Event & ev) {
* cb.notify_event(ev);
* },
* ev);
**/
template <typename Fn, typename... Tn>
void invoke_each(Fn && fn, Tn&&... args) {
running_ = true;
try {
for (size_type i = 0; i < size_; ++i)
fn(slot_[i].fn_, args...);
this->apply_deferred();
} catch(...) {
this->apply_deferred();
throw;
}
}
/** call (cb.*member_fn)(args...) for each registered callback.
*
* For faceted callbacks, @p member_fn is simply
* &Callback::some_facet_method
* (a pointer-to-member of the router base converts implicitly).
*
* Use:
* cbset.invoke(&Callback::notify_event, ev);
*
* MemberFn is deduced whole, so const/noexcept overloads and
* methods inherited from any base all work.
**/
template <typename MemberFn, typename... Tn>
requires fomo_callback_concept<Callback>
void invoke(MemberFn member_fn, Tn&&... args) {
this->invoke_each([member_fn](Callback & cb, auto&&... inner) {
(cb.*member_fn)(inner...);
},
args...);
}
/** call cb(args...) for each registered callback.
* For plain function-pointer callbacks.
**/
template <typename... Tn>
requires plain_callback_concept<Callback>
void invoke(Tn&&... args) {
this->invoke_each([](Callback & cb, auto&&... inner) {
cb(inner...);
},
args...);
}
///@}
private:
CallbackSet() = default;
/** remove callback @p id from the live region **/
bool remove_callback_impl(CallbackId id) noexcept {
for (size_type i = 0; i < size_; ++i) {
if (slot_[i].id_ == id) {
/* order is not significant: fill the hole from the end */
slot_[i] = slot_[size_ - 1];
--size_;
return true;
}
}
return false;
}
/** apply add/remove commands deferred during a callback run **/
void apply_deferred() noexcept {
running_ = false;
for (size_type i = 0; i < n_defer_; ++i) {
const slot_type & s = slot_[capacity_ + i];
if (s.is_add_) {
if (!this->is_full()) {
slot_[size_] = s;
slot_[size_].is_add_ = false;
++size_;
}
} else {
this->remove_callback_impl(s.id_);
}
}
n_defer_ = 0;
}
private:
/** extent of the live region of @ref slot_ **/
size_type capacity_ = 0;
/** number of live callbacks. Invariant: size_ <= capacity_ **/
size_type size_ = 0;
/** extent of the deferral region, which begins at slot_[capacity_] **/
size_type defer_capacity_ = 0;
/** number of pending deferred commands **/
size_type n_defer_ = 0;
/** true while callbacks are running; add/remove defer in that state **/
bool running_ = false;
/** live callbacks in [0, size_);
* deferred commands in [capacity_, capacity_ + n_defer_)
**/
slot_type slot_[];
}; /*CallbackSet*/
} /*namespace fn*/
} /*namespace xo*/
/* end CallbackSet.hpp */