Repository navigation
defer within nested state machines isn't dropped upon exit of nested state machine #253
Description
I'm trying to use defer within a nested state machine. In the simplest sequence of events, this will work, but the following example shows behavior that is undesirable for me:
Example:
#include <boost/sml.hpp>
#include <iostream>
#include <deque>
#include <queue>
#include <cassert>
namespace sml = boost::sml;
using namespace sml;
struct e1 {};
struct TO_S2 {};
struct TO_S1 {};
struct ENTER {};
struct EXIT {};
struct Nested {
auto operator()() const noexcept {
return make_transition_table(
*"s1"_s + event<TO_S2> / [] () { std::cout << "s1 + TO_S2 = s2" << std::endl; } = "s2"_s
, "s2"_s + event<e1> / defer
, "s1"_s + event<e1> / [] () { std::cout << "s1 + e1 [internal]" << std::endl; }
, "s2"_s + event<TO_S1> / [] () { std::cout << "s2 + TO_S1 = s1" << std::endl; } = "s1"_s
);
}
};
struct Outer {
auto operator()() const noexcept {
return make_transition_table(
*"Initial"_s + event<ENTER> / [] () { std::cout << "Initial + ENTER = Nested" << std::endl; } = state<Nested>
, state<Nested> + event<EXIT> / [] () { std::cout << "Nested + EXIT = Initial" << std::endl; } = "Initial"_s
, "Initial"_s + event<e1> / [] () { std::cout << "Initial: processing E1" << std::endl; }
);
}
};
int main()
{
sm<Outer, sml::defer_queue<std::deque>, sml::process_queue<std::queue>> sm{};
// Works as expected
sm.process_event(e1{});
sm.process_event(ENTER{});
assert(sm.is<decltype(state<Nested>)>("s1"_s));
sm.process_event(TO_S2{});
sm.process_event(e1{});
sm.process_event(TO_S1{});
sm.process_event(EXIT{});
// Undesirable Behavior
sm.process_event(ENTER{});
assert(sm.is<decltype(state<Nested>)>("s1"_s));
sm.process_event(TO_S2{});
sm.process_event(e1{}); // remove this line to see a difference
sm.process_event(EXIT{});
sm.process_event(ENTER{});
assert(sm.is<decltype(state<Nested>)>("s1"_s));
#if 1
sm.process_event(TO_S2{});
sm.process_event(TO_S1{});
#endif
return 0;
}There is a transition from the state s1 of the nested state machine Nested when the event e1 occurs. There is also a deferment of the event when the Nested state machine is in the state s2. When the Nested state machine is entered and the event is generated in s2, and then a transition to s1 is made, this works perfectly fine.
However, if I defer an event e1 while the Nested state machine is in s2, and then leave the nested state machine to return to the outer state machine, the following happens:
- The state machine does not handle the event
e1in the outer state machine. (The"Initial: processing E1"action is not executed.) This could be expected behavior, ifdeferis defined to only affect the context of the state machine it's placed in. - When I enter the
Nestedstate machine again, by default it will be in states1, but the previously deferred event is not triggered. I believe that to be correct, because the event should have been dropped once theNestedstate machine was exited. - However, if I perform a transition within the
Nestedstate machine back to thes1state (by first transitioning to another state), the previously queued event will now in fact be handled -- the last line of the example code iss1 + e1 [internal], which is the previously deferred event.
(Change #if 1 to #if 0 to see that in that case the event e1 is not handled at the end of the code, even though it's the first time that the Nested state machine is in s1 again after the defer.)
My intuition tells me that the defer queue of a Nested state machine should be cleared once the state machine is exited, so that re-entering the state machine really starts fresh in its initial state. Or that if the queue isn't cleared, that it should be applied to the main state machine instead, when the transition out of the nested state machine occurs, so I could at least add some handlers to clean up these kinds of events if they leak out. (Though that would break abstraction to a certain extent.)
However, even if you were to argue that the nested state machine were to keep the deferred events (which I don't think is useful), then the event should be handled when the nested state machine is entered (the first time it's in s1 again after the deferment), not only after an internal transition to s1.
I'd write a patch to drop the deferred queue upon entering a sub state machine, but I have to admit that I'm a bit lost in the internal design of Boost.SML. Is there any design document out there that describes the basic structure in form of an overview?
Activity
[-]defer within nested state machines doesn't track nested sub-state[/-][+]defer within nested state machines isn't dropped upon exit of nested state machine[/+]Fix issue boost-ext#253 nested defer queue leak on sub-sm exit
Rewrite README to document all fork improvements
PavelGuzenfeld commentedon May 28, 2026
Status on HEAD
84dbf15(2026-05-28): STILL PRESENTThe stale-deferred-event bug is reproducible on current master.
Minimal reproducer:
Root cause:
deferalways pushes to the root SM's defer queue (the one declared viadefer_queue<std::deque>on the outer SM). The nested SM has no queue of its own. When the outer SM exits theNestedcomposite state, the defer queue is not cleared or trimmed.process_defer_eventsreplays all queued events on the next outer-SM event dispatch. The replay ignores which state or sub-SM enqueued each event — it simply dispatches to the current state. When the outer SM is back inNested/s1, the old deferrede1is dispatched and handled bys1 + event<e1>.Three distinct behaviors described in the original report — all confirmed:
e1is not replayed inInit(correct —Inithas no handler).Nestedand staying ins1, the deferrede1is not replayed immediately (incorrect deferral semantics; it should fire here if it's going to fire at all).s1→s2→s1round-trip inside the re-enteredNested, the stalee1is replayed — because the internal transition triggersprocess_defer_events.Proposed fix direction:
Record a "high-water mark" (queue size) when entering a composite sub-state, and truncate the defer queue back to that mark when exiting the composite state. This would discard all events that were deferred within the sub-SM's lifetime.
This requires threading the enter/exit hooks for composite states through to the queue trim, which is non-trivial but confined to
update_current_state/on_exitlogic.A simpler stop-gap: expose a way to clear the defer queue from user code (e.g., as an action), so users can manually drain it on exit from a composite state.
fix: clear sub-SM defer queue when composite state is re-entered (issue
PavelGuzenfeld commentedon Oct 17, 2026
Fixed by #705, the repro no longer fires the stale deferred event on master bc13fcb.
PavelGuzenfeld commentedon Oct 17, 2026
Fixed by #705. The repro no longer fires the stale deferred event on master bc13fcb. Can be closed.
PavelGuzenfeld commentedon Oct 17, 2026
@kris-jusiak can you close this? Fixed or answered, see the comment above.