Overview
The design patterns module provides four header-only building blocks for embedded code: observer notification, an explicitly managed singleton, variadic visitor dispatch, and a compile-time finite state machine. None of them allocate on the heap or require RTTI or STL containers.
Observer and visitor dispatch through virtual functions. The FSM uses static dispatch only, and the singleton is a plain static wrapper with no virtual calls.
Observer
Fixed-capacity observer interfaces and an observable registry for embedded event fan-out. Use it when a subject must notify a bounded set of listeners without dynamic allocation.
Dependencies
castle/container/array.hppcastle/core/types.hppcastle/core/traits.hpp
Public API
| API | Description |
|---|---|
observer<T> | Abstract observer interface with notify(T const&). |
observer<void> | Abstract observer interface for parameterless notifications. |
observer<T, Rest...> | Multiple-inheritance helper that exposes all notify() overloads for unique payload types. |
observable<TObserver, N> | Stores up to N observer pointers in fixed-capacity inline storage. |
add_observer | Registers a non-null observer pointer; rejects duplicates and overflow. |
remove_observer | Unregisters a previously added observer pointer. |
notify_observers | Invokes notify(data) or notify() on every registered observer. |
Usage example
See samples/design_patterns/observer.cpp.
#include "castle/design_patterns/observer.hpp"
struct temperature_observer : castle::design_patterns::observer<uint16_t>
{
void notify(uint16_t const& value) override { last = value; }
uint16_t last = 0U;
};
Constraints & notes
- No heap allocation;
observablestores observer pointers incastle::container::array. - Lifetime management is manual: observers must outlive their registrations.
- Duplicate registrations are rejected.
- Dispatch uses virtual functions, so this header does not avoid virtual dispatch even though it remains RTTI-free.
Singleton
An explicitly controlled singleton wrapper that constructs one instance in aligned static storage with no heap allocation. Use it when a process-wide object must be created and destroyed deterministically by the caller.
Dependencies
castle/core/compiler.hpp,castle/core/error_handler.hpp,castle/core/traits.hpp,castle/core/types.hppcastle/memory/alignment.hpp,castle/memory/new.hpp,castle/utility/utility.hpp
Public API
| API | Description |
|---|---|
singleton<T> | Static-only wrapper around one T stored in aligned in-class storage. |
value_type | Alias for the managed type T. |
create(Args&&...) | Constructs the singleton in place; asserts if it already exists. |
destroy() | Destroys the instance; asserts if it does not exist. |
instance() | Returns a reference to the live object; asserts if not created. |
is_valid() | Reports whether a live object currently exists. |
Usage example
See samples/design_patterns/singleton.cpp.
#include "castle/design_patterns/singleton.hpp"
struct service
{
explicit service(uint32_t seed) : value(seed) {}
uint32_t value;
};
using global_service = castle::design_patterns::singleton<service>;
void run()
{
global_service::create(42U);
uint32_t const seed = global_service::instance().value;
global_service::destroy();
}Constraints & notes
- No heap allocation; construction uses placement new into aligned static storage.
- Lifetime is controlled explicitly by
create()anddestroy(). create()anddestroy()are not thread-safe; synchronize lifecycle changes externally.- Misuse is reported through Castle assertions instead of exceptions.
Visitor
Variadic visitor and visitable base helpers for classic visitor-style dispatch. Use it when concrete node types need a shared visitor interface with compile-time-checked overload sets.
Dependencies
castle/core/compiler.hppcastle/core/traits.hpp
Public API
| API | Description |
|---|---|
visitor<T1, Types...> | Combines multiple visit() overload requirements through recursive inheritance. |
visitor<T1> | Base case declaring one pure virtual visit(T1). |
visitable<T1, Types...> | Combines multiple accept() overload requirements through recursive inheritance. |
visitable<T1> | Base case declaring one pure virtual accept(T1&). |
Usage example
See samples/design_patterns/visitor.cpp.
#include "castle/design_patterns/visitor.hpp"
struct start_command;
struct stop_command;
struct command_visitor : castle::design_patterns::visitor<start_command&, stop_command&>
{
void visit(start_command&) override {}
void visit(stop_command&) override {}
};Constraints & notes
- No heap allocation or RTTI.
visit()andaccept()are pure virtual, so dispatch is runtime polymorphic rather than CRTP or static.- Parameter types must be unique; duplicates are rejected with
static_assert.
Finite state machine
A compile-time described finite state machine for embedded applications. States and events are C++ types, and a transition table maps an exact (source state, event type) pair to a target state. Optional guard and action functors provide decision and side-effect behavior without virtual dispatch or callback type erasure.
Dependencies
castle/core/compiler.hppcastle/core/traits.hppcastle/core/types.hpp
Public API
| API | Description |
|---|---|
fsm::empty_context | Empty context passed to guards and actions by the one-argument process_event() overload. |
fsm::always_guard | Default guard; accepts every event. |
fsm::no_action | Default action; does nothing. |
fsm::transition<From, Event, To, Guard, Action> | Compile-time transition row. Guard defaults to always_guard; Action defaults to no_action. |
fsm::event_result | Result enum: transitioned, ignored, or guard_rejected. |
fsm::state_machine<Initial, Rows...> | FSM type whose states are inferred from the initial state and all transition rows. |
state_machine::state_count / transition_count | Number of unique state types and transition rows. |
state_machine::state_id<State>() | Compile-time state index; rejects state types not present in the machine. |
state_machine::current_state_id() | Runtime index of the current state. |
state_machine::is_in_state<State>() | Tests the current state using a state type. |
state_machine::has_transition<State, Event>() | Compile-time query for a declared state/event pair. |
state_machine::process_event(event, context) | Processes an event with caller-owned application context. |
state_machine::process_event(event) | Processes an event with a temporary empty_context. |
state_machine::reset() | Restores the initial state without running any guard or action. |
Usage example
See samples/design_patterns/fsm.cpp for guards, actions, and multiple source states.
#include "castle/design_patterns/fsm.hpp"
struct locked {};
struct unlocked {};
struct key_turned {};
struct door_closed {};
using door_fsm = castle::design_patterns::fsm::state_machine<
locked,
castle::design_patterns::fsm::transition<locked, key_turned, unlocked>,
castle::design_patterns::fsm::transition<unlocked, door_closed, locked>
>;
int main()
{
door_fsm door;
door.process_event(key_turned{});
return door.is_in_state<unlocked>() ? 0 : 1;
}Constraints & notes
- State types are tags with no runtime state; mutable application data belongs in the context passed to
process_event(). - Event matching is exact on the decayed event type; there is no inheritance-based or runtime-polymorphic matching.
- Each
(source state, event type)pair may appear only once. A false guard returnsguard_rejectedand does not fall through to another row. - Guards and actions must be nothrow default-constructible,
noexcept, and callable asfunctor(context, event). - A self-transition runs its guard and action and returns
transitioned. reset()only changes the state index. It runs no guards, actions, or hooks.- Each instance stores one state index. Dispatch scans the rows in declaration order, so it is O(T) for T rows; state queries and reset are O(1).
- No internal synchronization. Protect an instance shared across tasks or interrupt contexts.