C CASTLE / MODULE NOTES
CASTLE/Modules/Design patterns

MODULE 07 / 15

Design patterns

Allocation-free building blocks for observer notification, explicitly controlled singletons, visitor dispatch, and compile-time finite state machines.

#include "castle/design_patterns/{observer,singleton,visitor,fsm}.hpp"

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.hpp
  • castle/core/types.hpp
  • castle/core/traits.hpp

Public API

APIDescription
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_observerRegisters a non-null observer pointer; rejects duplicates and overflow.
remove_observerUnregisters a previously added observer pointer.
notify_observersInvokes 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; observable stores observer pointers in castle::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.hpp
  • castle/memory/alignment.hpp, castle/memory/new.hpp, castle/utility/utility.hpp

Public API

APIDescription
singleton<T>Static-only wrapper around one T stored in aligned in-class storage.
value_typeAlias 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() and destroy().
  • create() and destroy() 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.hpp
  • castle/core/traits.hpp

Public API

APIDescription
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() and accept() 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.hpp
  • castle/core/traits.hpp
  • castle/core/types.hpp

Public API

APIDescription
fsm::empty_contextEmpty context passed to guards and actions by the one-argument process_event() overload.
fsm::always_guardDefault guard; accepts every event.
fsm::no_actionDefault 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_resultResult 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_countNumber 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 returns guard_rejected and does not fall through to another row.
  • Guards and actions must be nothrow default-constructible, noexcept, and callable as functor(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.