neo/event.hpp
provides a generic, declarative, zero-allocation, and
(nearly-)zero-overhead framework for defining, emitting, and handling
synchronous events within a single thread.
As a single example, this can be used to deliver operation progress information from a low-level API to a higher-level API:
struct ev_progress {
double value;
};
void download(string url) {
// Put a listener in scope
neo::listener l = [](ev_progress ev) {
print("Download is {}% complete", ev.value * 100);
};
// Run the download
auto filename = compute_filename(url);
download_to_file(url, filename);
}
void download_to_file(string url, path dest) {
// Open a file
ofstream ofile{dest};
download_to_ostream(ofile, url);
}
void download_to_ostream(ostream& out, string url) {
// Start the request
auto resp = init_request(url);
stream_response_body(out, resp);
}
void stream_response_body(ostream& out, http_response& resp) {
// Loop until we finish
while (!resp.done()) {
// Save some of the response body to the file
resp.recv_some_body(out);
// Calculate the progress and emit an event
double progress = resp.calculate_progress();
neo::emit(ev_progress{progress});
}
}
This is only a single example use case. One can define arbitrarily many event types and use them for whatever one deems important.
As another example, suppose we replace global operator new
and
operator delete
with versions that emit events:
struct ev_new {
void* ptr;
size_t size;
};
struct ev_delete {
void* ptr;
};
void* operator new(size_t size) {
void* ptr = real_allocator(size);
neo::emit(ev_new{ptr, size});
return ptr;
}
void operator delete(void* ptr) noexcept {
neo::emit(ev_delete{ptr});
real_deallocate(ptr);
}
Because the neo::emit()
will traverse arbitrarily many call stacks, we will
receive an event any time any component in the program calls the global new
and delete
. This information could then be recorded and later reconstructed to
form a memory profile of the program.
Whereas one could simply do this logging in the new
and delete
functions
themselves, this is more flexible in that it allows the rest of the program to
change and modify the behavior of these event handlers, potentially at runtime.
Additionally, for example, one part of the program could override the thread's
event handlers to suppress or modify the behavior of these events.
The neo-sqlite3
library dispatches events for the purpose of logging and
auditing. For example, neo::sqlite3::statement::step()
:
struct event::step {
statement& stmt;
errc rc;
};
// ...
errable<void> statement::step() noexcept {
auto rc = errc{::sqlite3_step(c_ptr())};
neo::emit(event::step{*this, rc});
return rc;
}
With this event, one can inspect every database operation, regardless of who requested it:
int main() {
neo::listener stmt_logger = [](neo::sqlite3::event::step ev) {
print("statement::step(): statement={}, result={}, row={}",
neo::repr(ev.stmt),
neo::repr(ev.rc),
neo::repr(stmt.row()));
};
// ... Rest of application goes here ...
}
This example will simply generate a log message each time someone advances a database statement.
neo/event.hpp
defines a small few APIs:
class listener<Handler, Event>
- A class template that holds an event handler and manages the listening stack. Use of CTAD is supported and recommended.auto listen(Handlers...)
- A function that generates a set of listeners for the given handlers. Allows you to declare several listeners and store them in a single local variable.auto listen<Event>(Handler)
- A function that listens for an event of typeEvent
and dispatches that toHandler
. Useful ifHandler
is generic.void emit(Events...)
- Function that emits the given events to their appropriate handlers.auto emit(Event)
- If given a single event,neo::emit
can return a value from the listener.auto bubble_event(Event)
- Called from within an event handler forEvent
, sends the given event to the next handler in the chain following the currently executing handler.
neo/event.hpp
does not require that the event type be marked up in any way.
There is no base class to inherit from, nor any specially required members. It
is recommended that events be given a qualified name, either with an ev_
prefix or by living in an event
/events
namespace
.
neo/event.hpp
places no semantic or syntactic requirements on event types.
There are two ways to generate an emit an event. The simplest is to pass the event object directly:
neo::emit(ev_progress{.value = calc_progress()});
neo::emit
will conditionally dispatch the given event. If there is no listener
in the current thread then event will be dropped.
Even still, this direct approach will construct the event object, which itself may be expensive to compute. Unless the compiler can safely shift the event construction to be within the conditional, we will pay for construction of the event regardless of whether there is a listener.
As one solution conditionally call emit()
:
if (neo::has_listener<ev_progress>()) {
neo::emit(ev_progress{.value = calc_progress()});
}
but this might look a bit ugly, especially if we have a lot of events we wish to
emit. Instead, we have a second method of generating an event: emit()
allows
one to pass an event factory function:
neo::emit([&] { return ev_progress{.value = calc_progress()}; });
The event factory should be invocable with zero arguments and return the event that should be emitted.
With neo/tl.hpp
, this can be further reduced to
neo::emit(NEO_TL(ev_progress{.value = calc_progress()}));
However, this is such a common and convenient pattern that neo/event.hpp
defines a macro:
NEO_EMIT(ev_progress{.value = calc_progress()});
If one wishes to handle an event, there are a few APIs, but they all boil down
to the neo::listener<Handler, EventType>
class template. To start, a listener
can be declared and its type arguments deduced via CTAD:
neo::listener on_progress = [&](ev_progress pr) {
update_progress_gui(pr.value);
};
Additionally, the Handler
parameter can be fulfilled alone, and the
EventType
will be deduced:
struct handler_group {
neo::listener<decltype([](ev_progress) {
update_progress_gui(pr.value);
})> on_progress;
};
e.g. this is required to declare a listener as a member of a class (until we someday get non-static data member type deduction).
One can also generate a listener by calling the listen
function template:
auto on_progress = neo::listen([](ev_progress pr) {
update_progress_gui(pr.value);
});
The neo::listen
function can be called with multiple invocable objects, and
they will each generate a listener stored in a tuple in the return value of
listen()
. The return type of neo::listen
should be deduced and not specified
manually.
All of the above listener-declaration methods have a requirement in common: The invocable handler must be a concrete invocable that accepts a single argument. For example, this will not work:
neo::listener on_progress = [](auto pr) { ... };
CTAD has no way to figure out what event type the given closure is supposed to handle.
There is still a solution though, and will be required if you wish to use a generic handler:
struct event_logger {
ostream& out;
void operator()(const auto& event) {
out << "Got an event: " << neo::repr(event) << '\n';
}
};
int main() {
event_logger logger{std::cerr};
auto on_progress = neo::listen<ev_progress>(logger);
auto on_warning = neo::listen<ev_warning>(logger);
auto on_error = neo::listen<ev_error>(logger);
}
In the above, we create a single handler object logger
which is has a generic
operator()
. We generate three listeners by calling neo::listen
and providing
an explicit template argument specifying the event type that we wish to listen
for.
When neo::emit()
finds a listener for an event, it will pass the event to that
handler and return. If we constructed a listener for an event E
when there was
already a prior listener in the thread for E
, the prior listener will not
receive events for E
. That is, events do not "bubble up" be default.
An event handler can explicitly bubble-up the event during its own execution by
passing the event to neo::bubble_event
:
neo::listener on_warning = [](ev_warning ev) {
print("Something's fishy: {}", ev.message);
neo::bubble_event(ev);
};
bubble_event()
will synchronously dispatch the given event to the next
listener in the chain following the listener that called bubble_event()
.
Note: If bubble_event(E)
is called outside of the currently executing E
handler, the program will terminate!
The event object does not need to be the same object that was passed to the handler. It need only be of the same type as the handler. Additionally, because bubbling happens explicitly, one can make event bubbling conditional:
neo::listener warning_filter = [](ev_warning ev) {
if (should_keep_warning(ev)) {
neo::bubble_event(ev);
}
};
An event type itself can declare that it is auto-bubbling by providing a static
constant expression event_bubbles
:
struct my_event {
std::string_view message;
enum { event_bubbles = true };
};
This will cause the event to bubble-up the chain of handlers automatically. It
is possible to check whether a given event type will auto-bubble using the
neo::event_bubbles<E>
concept. It is illegal to call bubble_event()
on an
auto-bubbling event.
If a handler wishes to prevent this auto-bubbling behavior, the handler may call
neo::cancel_bubbling(e)
. Like with bubble_event
, this may only be called
within the currently executing event handler for e
. It is illegal to call
cancel_bubbling()
for a type that does not auto-bubble.
Note that if an event is auto-bubbling (as above), it is not allowed to also
have a non-void
emit_result_t
(discussed below).
If you know that a callee is going to emit an event and you want to prevent it
from reaching beyond your current scope, you can block events using a
neo::event_blocker<E>
:
void download_no_progress(string url) {
// Prevent ev_progress from escaping
neo::event_blocker<ev_progress> no_progress;
download(url);
}
neo::event_blocker
installs a listener that is a no-op and calls
cancel_bubbling()
on events. This may be useful to prevent events from firing
during their own handler and causing an infinite recursion:
void do_operation_with_logging() {
neo::listener on_error = [](ev_error err) {
// Block event errors from the `log_error()` function
neo::event_blocker block_errors{err};
// log_error() might fire more ev_error events, and we don't want
// to infinitely recurse on itself.
log_error(err);
};
do_operation_with_errors();
}
If an event type has a nested type emit_result
, then it is treated as an event
with a "emit result".
The type of an event's emit-result can be taken from neo::emit_result_t<E>
. If
E
does not define a nested type emit_result
, then emit_result_t<E>
is
void
. If the E::emit_result
type is void
, then it is indistinguishable
from an event that has no emit result.
If the emit_result_t
is non-void
, then the event's handler has the option to
return a value. The return value of the handler must be convertible to the
emit_result_t
of the event that it handles.
When an event with a non-void
emit-result is passed to neo::emit
as the sole
argument, neo::emit
will return an instance of that emit-result. If there is
an active and listening handler that returns non-void
, the return value of
that handler will be returned from neo::emit
. If there is no such handler,
then a default result value will be either default constructed or obtained by
calling the .default_emit_result()
member function on the emitted event
object. For this reason, if emit_result_t<E>
is non-void
, then:
- For
const E& ev
,ev.default_emit_result()
must be a valid expression evaluating to a value convertible toemit_result_t<E>
, OR emit_result_t<E>
must be default-constructible.
As an example use case, this can be used to implement generic cancellation for a synchronous API:
struct ev_http_progress {
size_t expected_size;
size_t downloaded_size;
// An event handler can tell us to either stop or to continue
enum emit_result {
keep_going,
cancel,
};
};
// Download a large file. Can be stopped with the given stop_token
void download_file(string url, stop_token stop) {
neo::listen cancellation = [&](ev_http_progress progress) {
// Update our UI
update_progress_ui(progress);
// Check whether we should stop or continue
if (stop.stop_requested()) {
return progress.cancel;
} else {
return progress.keep_going;
}
};
do_long_running_download(url);
}
// ... Several API layers later ...
void write_http_response_body(http_repsonse resp, std::ostream& out) {
size_t acc_size = 0;
while (!resp.done()) {
// Receive a part and write it
auto part = resp.recv_some();
out.write(part.data(), part.size());
acc_size += size;
// Send an event
auto do_cancel = neo::emit(
ev_http_progress{
.expected_size = resp.content_length(),
.downloaded_size = acc_size,
});
// Check if a handler requested that we stop
if (do_cancel == ev_http_progress::cancel) {
break;
}
}
}
Note that if an event has a non-void
emit_result_t
, then that event
cannot also be an auto-bubbling event.
Unlike exception handling, where catch (const some_base_class&)
will handle
throw some_derived_class()
, this event system will not see a link between a
base class and a derived class.
To work around this issue, we can instead use emit_as
to tell neo::emit
to
have emit()
automatically cast the event to another type before propagating
the event. Here is an example using the tired "Animal" inheritance example:
struct animal_event {
virtual string sound() const = 0;
using emit_as = animal_event;
};
struct cat_event : animal_event {
string sound() const override { return "meow"; }
};
struct dog_event : animal_event {
string sound() const override { return "bark"; }
};
Now, if we ever emit()
a subclass of animal_event
, it will automatically be
emitted and handled as an animal_event
:
void foo() {
neo::listener = [](const animal_event& ev) {
print("This animal says '{}'", ev.sound());
};
neo::emit(cat_event{});
neo::emit(dog_event{});
}