Typed publish-subscribe for C++23 that compiles to direct calls when the receivers are known.
Sub0Pub is a header-only library for synchronous, type-safe message delivery. Write publishers and subscribers once; each message type then says whether it is delivered through a bounded runtime broker, where subscribers subscribe and unsubscribe independently, or by direct calls to a known set of receivers. The core does not allocate broker storage from the heap; optional policies and application callbacks have their own costs.
Status: v2.0.0-alpha is CI-tested, but has not yet been field-tested.
struct TemperatureReading { int celsius; };
class Sensor : public sub0::Publish<TemperatureReading> {
public:
void sample(int celsius) noexcept { sub0::publish(*this, TemperatureReading{celsius}); }
};
class Display : public sub0::Subscribe<TemperatureReading> {
public:
void receive(const TemperatureReading& reading) noexcept { /* show it */ }
};As written, a Display subscribes when it is constructed and unsubscribes when it is destroyed: the runtime broker
delivers through a bounded, per-type table. When the receivers of a type are known, name them beside the type and
the same code compiles to direct calls, with no table, run-time subscription or virtual call behind it:
class Display;
extern Display display;
struct TemperatureReading {
int celsius;
using sub0_config = sub0::config<sub0::StaticTo<&display>>;
};flowchart LR
P[Publish T] -->|publish| C{T's configuration}
C -->|nothing said| B[Runtime broker]
C -->|StaticTo| R[Listed receivers, direct calls]
C -->|StaticFirst| F[Listed receivers, direct calls]
F --> B
B -->|delivery through its table| S[Subscribe T, subscribed at run time]
| Your case | Say on the type | Main constraint | |
|---|---|---|---|
| 1 | Receivers subscribe and unsubscribe independently. Start here. | nothing | Fixed capacity; a subscription can fail when the table is full. |
| 2 | The receivers are a closed set of objects with static storage. | sub0::StaticTo<&a, &b> |
The list is the whole truth: nothing subscribes or unsubscribes at run time. |
| 3 | A fixed core, plus receivers that subscribe and unsubscribe at run time. | sub0::StaticFirst<&a> |
The broker's cost stays on every publication. |
A publication that reaches no receiver is treated as a mistake: a debug build reports it, and a StaticTo list
nobody can receive from does not compile. A type for which that is expected says sub0::AllowNoReceivers. An
audit build reports every such mistake a run makes, and
which message types are ready to name their receivers.
See basic pub/sub for subscribing and unsubscribing through object lifetime, promote to static for one application built both ways from one source, and dynamic diagnostics for a fixed receiver with probes that subscribe at run time beside it.
Receivers that are local objects, several independent wirings of one message type, a receiver that stops a publication, and a transport bound beside local receivers are composed explicitly, with plain receiver classes:
auto wiring = sub0::wire(display, logger);
wiring.publish(TemperatureReading{22});wire(...), StaticWiring, Sink<T>, Forward and BrokerPort<T> each exist for a specific case; the
usage guide lists which.
Requirements: C++23 and CMake 3.21+. The CMake target propagates the language requirement; direct header users must select C++23 themselves.
find_package(Sub0Pub REQUIRED)
target_link_libraries(MyApp PRIVATE Sub0Pub::Sub0Pub)Alternatively, add Sub0Pub as a subdirectory:
add_subdirectory(Sub0Pub)
target_link_libraries(MyApp PRIVATE Sub0Pub::Sub0Pub)Build and run the tests from a clone:
cmake --preset default
cmake --build --preset default
ctest --preset default#include <sub0pub/sub0pub.hpp> includes the full library. For smaller includes, see the
header map.
The README is an overview; detailed behavior, constraints, and complete examples live in focused guides:
| Guide | Covers |
|---|---|
| Usage and wiring | Which delivery to choose by case, moving a type to direct calls, explicit wiring, callback behavior, subscriber lifetime, and examples |
| Per-type configuration | Where a type's configuration goes, topology, dispatch, context, capacity, filtering, cancellation, locking, scoped domains, and project defaults |
| IPC and transports | Routes, forwarding, stream serialization, layout checks, and wire-format responsibilities |
| Runnable examples | Standalone programs for the current API, organized by use case |
| Design and contracts | Design rationale, measured constraints, and known limitations |
| Comparisons | Architectural trade-offs versus related libraries; not a speed ranking |
| Migration notes | Changes to consider when updating an existing application |
Sub0Pub focuses on synchronous typed delivery, fixed-capacity broker storage, and direct calls when the receiver set is known. It does not provide an event queue, scheduler, or GUI event loop. Whether that is a good fit depends on the application; the comparison notes describe the trade-offs without claiming cross-library performance superiority.
Performance claims are measured against equivalent hand-written work. See the current performance baseline, measurement evidence, and compile-time measurements. The benchmark suite can be built with the tests:
cmake --build --preset default --target Sub0Pub_BenchCI covers GCC, Clang, AppleClang, and MSVC on Linux, macOS, and Windows, with sanitizer configurations. This testing does not replace integration testing on a project's target platform.
MIT License -- Copyright (c) 2018 Craig Hutchinson