Skip to content

fix: guard taking const State& now sees live pool state (issue #530)#681

Merged
kris-jusiak merged 1 commit into
boost-ext:masterfrom
PavelGuzenfeld:fix/issue-530-const-ref-state-dep
May 26, 2026
Merged

fix: guard taking const State& now sees live pool state (issue #530)#681
kris-jusiak merged 1 commit into
boost-ext:masterfrom
PavelGuzenfeld:fix/issue-530-const-ref-state-dep

Conversation

@PavelGuzenfeld
Copy link
Copy Markdown
Contributor

Problem

When a state machine uses BOOST_SML_CREATE_DEFAULT_CONSTRUCTIBLE_DEPS and an action mutates a state object via State& while a subsequent guard reads it via const State&, the guard received a default-constructed copy instead of the live, mutated value.

Root cause: ignore::non_events produced two separate dep_list entries — State& and const State& — which caused two independent pool slots to be allocated and initialised from the same default-constructed object. Mutations via the mutable slot (State&) were invisible to the const-ref slot (const State&).

Minimal repro

#define BOOST_SML_CREATE_DEFAULT_CONSTRUCTIBLE_DEPS
#include <boost/sml.hpp>
struct State { int id{}; };
struct connect { int id{}; };

struct c {
  auto operator()() noexcept {
    using namespace sml;
    const auto set   = [](const connect& ev, State& s) { s.id = ev.id; };
    const auto check = [](const connect& ev, const State& s) {
      return s.id == 42; // FAILED: s.id was 0 (stale copy)
    };
    return make_transition_table(
      *state<struct idle> + event<connect> / set         = state<State>,
       state<State>       + event<connect>[check]        = X
    );
  }
};

sml::sm<c> sm;
sm.process_event(connect{42});  // sets State::id = 42
sm.process_event(connect{99});  // guard should see id==42, but saw 0

Fix (two parts)

1. ignore::non_events normalisation — map const U&U& before inserting into the dep_list, so mutable and const references share a single pool slot:

template <class T>
struct non_events {
  using stripped_ = aux::remove_const_t<aux::remove_reference_t<T>>;
  using norm_ = aux::conditional_t<aux::is_same<T, const stripped_&>::value, stripped_&, T>;
  using type =
      aux::conditional_t<..., aux::type_list<>, aux::type_list<norm_>>;
};

2. New get_arg overload for const T& — looks up T& in the pool (not const T&) so both call-sites bind to the same live object:

template <class T, class TEvent, class Tsm, class TDeps,
          __BOOST_SML_REQUIRES(!aux::is_same<..., T>::value)>
constexpr const T& get_arg(const aux::type_wrapper<const T&>&,
                            const TEvent&, Tsm&, TDeps& deps) {
  return aux::get<T&>(deps);
}

Test

test const_ref_state_dep_sees_live_state added to test/ft/dependencies.cpp (compiled under BOOST_SML_CREATE_DEFAULT_CONSTRUCTIBLE_DEPS).

Closes #530

…ext#530)

Root cause: when both a mutable action (State&) and a const guard
(const State&) name the same type, ignore::non_events produced two
separate dep_list entries — State& and const State& — which caused
two independent pool slots to be allocated and initialised from the
same default-constructed object.  Subsequent mutations via the mutable
slot were therefore invisible to the const-ref slot.

Fix (two parts):
1. ignore::non_events now normalises 'const U&' → 'U&' before adding
   the type to the dep_list, so both the mutable action and the const
   guard share a single pool slot.
2. A new get_arg overload for 'const T&' parameters looks up 'T&' in
   the pool (instead of 'const T&'), ensuring both call-sites bind to
   the same live object.

Test: const_ref_state_dep_sees_live_state in test/ft/dependencies.cpp
(compiled under BOOST_SML_CREATE_DEFAULT_CONSTRUCTIBLE_DEPS) verifies
that a guard taking const State& sees the id value written by the
preceding set action.
@PavelGuzenfeld PavelGuzenfeld force-pushed the fix/issue-530-const-ref-state-dep branch from 1a44783 to 9555375 Compare May 26, 2026 06:29
@kris-jusiak kris-jusiak merged commit fb7e081 into boost-ext:master May 26, 2026
5 checks passed
@jefftrull
Copy link
Copy Markdown
Contributor

This has been a thorn in my side, thanks for repairing it!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Guards' access to state data compiles nicely but doesn't follow principle of least surprise

3 participants