Boost::ext::SML
SML
开发记录
在开发时遇到了一些问题,在这里记录一下。
- 在创建状态机对象时,其构造函数会直接根据转移表开始运行状态机,状态机如果因没有收到 事件 无法转移到下一个状态,将会阻塞状态机运行。
- 在单线程程序中体现为暂停状态机,继续运行后续代码。
- 在多线程程序(状态机单独线程)中体现为状态机线程挂起,等待其他线程输入事件。
- 在存在子状态机的状态机中,切换到子状态机之后似乎需要再发送一个事件才会继续执行子状态机的操作,这里没有写demo测试,可能是个人代码问题。
- 外部Controller对状态机的控制,本来是想通过map映射event,外部输入int来发送事件,实现的时候发现问题很多。参考了一下其他人的代码,可以使用switch/case来控制,但是检索效率很低,最后选用跳表(jump_table)来实现,代码也更清晰一些。
install
包含目录引入include文件夹即可
包含内容:
boost/sml.hpp boost/sml/utility/dispatch_table.hpp
Transition Table DSL
- Postfix Notation
Expression | Description |
---|---|
state + event [ guard ] | internal transition on event e when guard |
src_state / [] {} = dst_state | anonymous transition with action |
src_state / [] {} = src_state | self transition (calls on_exit/on_entry) |
src_state + event = dst_state | external transition on event e without guard or action |
src_state + event [ guard ] / action = dst_state | transition from src_state to dst_state on event e with guard and action |
src_state + event [ guard && (![]{return true;} && guard2) ] / (action, action2, []{}) = dst_state | transition from src_state to dst_state on event e with guard and action |
return make_transition_table(
*"idle"_s / [] { std::cout << "anonymous transition" << std::endl; } = "s1"_s
// src_state / [] {} = dst_state
, "s1"_s + event<e1> / [] { std::cout << "internal transition" << std::endl; }
// state + event [ guard ]
, "s1"_s + event<e2> / [] { std::cout << "self transition" << std::endl; } = "s1"_s
// src_state / [] {} = src_state
, "s1"_s + sml::on_entry<_> / [] { std::cout << "s1 entry" << std::endl; }
, "s1"_s + sml::on_exit<_> / [] { std::cout << "s1 exit" << std::endl; }
, "s1"_s + event<e3> / [] { std::cout << "external transition" << std::endl; } = X
// src_state + event = dst_state
);
int main() {
using namespace sml;
sm<transitions> sm;
//anonymous transition
//s1 entry
sm.process_event(e1{});
//internal transition
sm.process_event(e2{});
//s1 exit
//self transition
//s1 entry
sm.process_event(e3{});
//s1 exit
//external transition
assert(sm.is(sml::X));
return 0;
}
上面代码的时序图如下
不带guard、action的状态转移
#include<boost/sml.hpp>
#include<cassert>
namespace sml = boost::sml;
namespace {
struct e1{};
struct e2{};//事件
class idle;
class s1; //状态
struct transitions{
auto operator()() const noexcept{
using namespace sml;
return make_transition_table( //构建状态事件表
*state<idle> /[] {} = state<s1>,
state<s1>+event<e1> /[]{},
/////
// src_state + event [ guard && (![]{return true;} && guard2) ] / (action, action2, []{}) = dst_state
state<s1>+event<e2> /[]{} = X //最终状态X
);
}
}
};
int main()
{
using namespace sml;
sm<transitions>sm; //构造状态机
sm.process_event(e1{});
///
sm.process_event(e2{});
assert(sm.is(sml::X));
return 0;
}
带有guard、action的状态转移
guard可以用来限制状态机的状态转移,只有当guard中的布尔表达式为真时,才会发生状态转移。action相当于触发器,只有状态机沿某条转移边转移时才会执行操作。
换句话说:
当且仅当
[ guard ]
内为 真 时,状态机状态才会转移,action才会执行。
#include <boost/sml.hpp>
#include <cassert>
#include <iostream>
#include <typeinfo>
namespace sml = boost::sml; // 命名空间别名
namespace {
// 定义事件类型
struct e1 {};
struct e2 {};
struct e3 {};
struct e4 {};
struct e5 {};
// 声明一个辅助函数的实现
bool guard2_impl(int i);
// 定义状态机中的动作和条件
struct actions_guards {
using self = actions_guards;
auto operator()() {
using namespace sml;
// 定义guard1条件
auto guard1 = [] {
std::cout << "guard1" << std::endl;
return true;
};
// 使用wrap函数将辅助函数guard2_impl转换为guard2条件
auto guard2 = wrap(&guard2_impl);
// 定义action1动作
auto action1 = [](auto e) {
std::cout << "action1: " << typeid(e).name() << std::endl;
};
// 定义action2结构体,重载函数调用运算符作为动作
struct action2 {
void operator()(int i) {
assert(42 == i);
std::cout << "action2" << std::endl;
}
};
// 创建状态机的转移表
return make_transition_table(
*"idle"_s + event<e1> = "s1"_s, // 初始状态到s1状态的转移,触发事件为e1
"s1"_s + event<e2> [ guard1 ] / action1 = "s2"_s, // s1到s2的转移,带有guard1条件和动作action1
"s2"_s + event<e3> [ guard1 && ![] { return false;} ] / (action1, action2{}) = "s3"_s, // s2到s3的转移,带有复合条件和多个动作
"s3"_s + event<e4> [ !guard1 || guard2 ] / (action1, [] { std::cout << "action3" << std::endl; }) = "s4"_s, // s3到s4的转移,带有条件组合和匿名动作
"s3"_s + event<e4> [ guard1 ] / ([] { std::cout << "action4" << std::endl; }, [this] { action4(); } ) = "s5"_s, // s3到s5的转移,带有条件和多个动作(包括成员函数动作)
"s5"_s + event<e5> [ &self::guard3 ] / &self::action5 = X // s5到终止状态的转移,带有成员函数条件和成员函数动作
);
}
// 成员函数guard3的实现
bool guard3(int i) const noexcept {
assert(42 == i);
std::cout << "guard3" << std::endl;
return true;
}
// 成员函数action4的实现
void action4() const {
std::cout << "action4" << std::endl;
}
// 成员函数action5的实现
void action5(int i, const e5&) {
assert(42 == i);
std::cout << "action5" << std::endl;
}
};
// 辅助函数guard2_impl的实现
bool guard2_impl(int i) {
assert(42 == i);
std::cout << "guard2" << std::endl;
return false;
}
}
// 主函数
int main() {
actions_guards ag{}; // 创建actions_guards对象
sml::sm<actions_guards> sm{ag, 42}; // 创建状态机对象
sm.process_event(e1{}); // 处理事件e1
sm.process_event(e2{}); // 处理事件e2
sm.process_event(e3{}); // 处理事件e3
sm.process_event(e4{}); // 处理事件e4
sm.process_event(e5{}); // 处理事件e5
assert(sm.is(sml::X)); // 断言状态机是否达到终止状态
return 0;
}
/* output:
guard1
action1: N12_GLOBAL__N_12e2E
guard1
action1: N12_GLOBAL__N_12e3E
action2
guard1
guard2
guard1
action4
action4
guard3
action5
*/
带数据的状态转移
状态机中的数据通过event传入临时数据,状态机的成员变量则是所有状态的共享数据。
#include <boost/sml.hpp>
#include <cassert>
#include <iostream>
namespace sml = boost::sml;
namespace {
struct connect {
int id{};
};
struct disconnect {};
struct interrupt {};
struct Disconnected {};
struct Connected {
int id{}; // per state data
};
struct Interrupted {
int id{}; // per state data
};
class data {
using Self = data;
public:
explicit data(const std::string& address) : address{address} {}
auto operator()() {
using namespace boost::sml;
const auto set = [](const auto& event, Connected& state) { state.id = event.id; };
const auto update = [](Connected& src_state, Interrupted& dst_state) { dst_state.id = src_state.id; };
return make_transition_table(
* state<Disconnected> + event<connect> / (set, &Self::print) = state<Connected>
, state<Connected> + event<interrupt> / (update, &Self::print) = state<Interrupted>
, state<Interrupted> + event<connect> / (set, &Self::print) = state<Connected>
, state<Connected> + event<disconnect> / (&Self::print) = X
);
}
private:
void print(Connected& state) { std::cout << address << ':' << state.id << '\n'; };
std::string address{}; // shared data between states
};
}
int main() {
data d{std::string{"127.0.0.1"}};
sml::sm<data> sm{d, Connected{1}}; //传入address,以及data结构中Connected结构体的成员变量id = 1
sm.process_event(connect{1024}); //通过event传入connnect结构体的成员变量id = 1024,同时进行状态转移
sm.process_event(interrupt{});
sm.process_event(connect{1025});
sm.process_event(disconnect{});
assert(sm.is(sml::X));
return 0;
}
使用deque和queue实现Defer/Process(延迟/运行)
有时需要让状态机处于某个状态时不去处理外来事件,而是先保存下来,之后再去处理。
可以使用sml::defer_queue
来实现上述功能,在发生状态转移后,sml::defer_queue中的事件会根据先进先出的顺序依次check(),当deferred events
不能发生转移时,保留其在队列中。
注意这里sml::defer_queue
不能用std::queue
实例化,只能用std::deque
否则会报错。
具体实现使用类似sm<defer_and_process,sml::defer_queue<std::deque>,sml::process_queue<std::queue>> sm
#include <boost/sml.hpp>
#include <cassert>
#include <deque>
#include <queue>
namespace sml = boost::sml;
namespace {
struct e1 {};
struct e2 {};
struct e3 {};
struct e4 {};
struct defer_and_process {
auto operator()() const noexcept {
using namespace sml;
return make_transition_table(
*"idle"_s + event<e1> / defer
, "idle"_s + event<e2> = "s1"_s
, "s1"_s + event<e1> = "s2"_s
, "s2"_s + event<e3> / process(e4{})
, "s2"_s + event<e4> = X
);
}
};
}
int main() {
using namespace sml;
sm<defer_and_process, sml::defer_queue<std::deque>, sml::process_queue<std::queue>> sm;
// 延迟队列策略以使用std::queue启用延迟事件
assert(sm.is("idle"_s));
sm.process_event(e1{});
assert(sm.is("idle"_s));
sm.process_event(e2{}); /// e2触发 idle -> s1 同时 s1 -> s2 (通过延迟队列里的e1事件)
assert(sm.is("s2"_s));
sm.process_event(e3{}); /// e3触发 s2.process(e4) -> X (通过 e4 事件)
assert(sm.is(sml::X));
return 0;
}
多维度不交叉控制状态机
一个状态机中可以同时存在多个不相交的区域,这使得我们同时可以从不同的维度去控制状态机。
#include <boost/sml.hpp>
#include <cassert>
namespace sml = boost::sml;
namespace {
struct e1 {};
struct e2 {};
struct e3 {};
struct orthogonal_regions {
auto operator()() const noexcept {
using namespace sml;
return make_transition_table(
*"idle"_s + event<e1> = "s1"_s //初始状态1
, "s1"_s + event<e2> = X
,*"idle2"_s + event<e2> = "s2"_s //初始状态2 与 初始状态1 同时接收事件
, "s2"_s + event<e3> = X
);
}
};
} // namespace
int main() {
sml::sm<orthogonal_regions> sm;
using namespace sml;
assert(sm.is("idle"_s, "idle2"_s));
sm.process_event(e1{});
assert(sm.is("s1"_s, "idle2"_s));
sm.process_event(e2{});
assert(sm.is(X, "s2"_s));
sm.process_event(e3{});
assert(sm.is(X, X));
return 0;
}
保存历史记录状态模式
通过"idle"_s(H)
可以将一个状态机标识为history模式(同时它也是该状态机的初始状态)。标识为history模式意味着每次进入该状态机时被激活的状态是最近使用的状态(上一次离开时的最后的激活状态)。
看起来也可以实现状态的嵌套。
#include <boost/sml.hpp>
#include <cassert>
#include <iostream>
namespace sml = boost::sml;
namespace {
struct sub {
auto operator()() const noexcept {
using namespace sml;
return make_transition_table(
"s1"_s <= "idle"_s(H) + "e1"_e / [] { std::cout << "in sub" << std::endl; }
, X <= "s1"_s + "e2"_e / [] { std::cout << "in sub again" << std::endl; } //第二次进入时sub状态已经处于s1状态了
);
}
};
struct history {
auto operator()() const noexcept {
using namespace sml;
return make_transition_table(
state<sub> <= *"idle"_s + "e1"_e / [] { std::cout << "enter sub" << std::endl; }
, "s1"_s <= state<sub> + "e3"_e / [] { std::cout << "exit sub" << std::endl; }
, state<sub> <= "s1"_s + "e4"_e / [] { std::cout << "enter sub again" << std::endl; }
);
}
};
} // namespace
int main() {
sml::sm<history> sm;
using namespace sml;
sm.process_event("e1"_e());
//enter sub
sm.process_event("e1"_e());
//in sub
sm.process_event("e3"_e());
// exit sub
sm.process_event("e4"_e());
// enter sub again
sm.process_event("e2"_e());
// in sub again (history)
return 0;
}
运行顺序调度 make_dispatch_table
可以通过给事件标号,然后在运行时通过标号来表示发生的事件进行转移。
既可以通过定义静态变量来标号,也可以通过继承sml::utility::id
来标号。
#include <boost/sml/utility/dispatch_table.hpp>
#include <boost/sml.hpp>
#include <cassert>
namespace sml = boost::sml;
namespace {
struct runtime_event {
int id = 0;
};
struct event1 {
static constexpr auto id = 1; //静态变量
event1(const runtime_event &) {}
};
struct event2: sml::utility::id<2> {}; //继承sml::utility::id
struct dispatch_table {
auto operator()() noexcept {
using namespace sml;
return make_transition_table(
*"idle"_s + event<event1> = "s1"_s
, "s1"_s + event<event2> = X
);
}
};
}
int main() {
sml::sm<dispatch_table> sm;
auto dispatch_event = sml::utility::make_dispatch_table<runtime_event, 1 /*min*/, 5 /*max*/>(sm);
{
runtime_event event{1}; //id为1的事件
dispatch_event(event, event.id); //执行状态转移
}
{
runtime_event event{2};
dispatch_event(event, event.id);
}
assert(sm.is(sml::X));
return 0;
}
错误捕获
我们可以在状态转移表中定制状态机发生异常时的行为,这里异常既可以是std::exception
,也可以是某些不正常的转移。
#include <boost/sml.hpp>
#include <cassert>
#include <iostream>
#include <stdexcept>
namespace sml = boost::sml;
namespace {
struct some_event {};
struct error_handling {
auto operator()() const {
using namespace sml;
return make_transition_table(
*("idle"_s) + "event1"_e / [] { throw std::runtime_error{"error"}; } //当初始状态接收到e1时,抛出runtime_error异常
, "idle"_s + "event2"_e / [] { throw 0; }
, *("exceptions handling"_s) + exception<std::runtime_error> / [] { std::cout << "exception caught" << std::endl; } //当异常处理状态接收到runtime_error异常时 输出exception caught表示捕获到了该异常
, "exceptions handling"_s + exception<_> / [] { std::cout << "generic exception caught" << std::endl; } = X //当异常处理状态接收到其他消息时,输出generic exception caught 转移状态为X
, *("unexpected events handling"_s) + unexpected_event<some_event> / [] { std::cout << "unexpected event 'some_event'" << std::endl; } // 当意外事件处理状态接收到 some_event时,输出 unexpected event 'some_event'
, "unexpected events handling"_s + unexpected_event<_> / [] { std::cout << "generic unexpected event" << std::endl; } = X //当意外事件处理接收到其它事件时,输出generic unexpected event,转移状态为X
);
}
};
} // namespace
int main() {
using namespace sml;
sm<error_handling> sm;
sm.process_event("event1"_e()); // throws runtime_error
assert(sm.is("idle"_s, "exceptions handling"_s, "unexpected events handling"_s));
sm.process_event("event2"_e()); // throws 0
assert(sm.is("idle"_s, X, "unexpected events handling"_s));
sm.process_event(some_event{}); // unexpected event
assert(sm.is("idle"_s, X, "unexpected events handling"_s));
sm.process_event(int{}); // unexpected any event
assert(sm.is("idle"_s, X, X));
return 0;
}
调试 testing::sm
[Boost::ext].SML
为我们提供了set_current_states
成员函数来方便我们更好地测试与调试。
#include <boost/sml.hpp>
#include <cassert>
namespace sml = boost::sml;
namespace {
struct e1 {};
struct e2 {};
struct e3 {};
struct data {
int value = 0;
};
struct testing {
auto operator()() const noexcept {
using namespace sml;
const auto guard = [](data& d) { return !d.value; };
const auto action = [](data& d) { d.value = 42; };
return make_transition_table(
*"idle"_s + event<e1> = "s1"_s
, "s1"_s + event<e2> = "s2"_s
, "s2"_s + event<e3> [guard] / action = X // transition under test
);
}
};
} // namespace
int main() {
using namespace sml;
data fake_data{0};
sml::sm<::testing, sml::testing> sm{fake_data};
sm.set_current_states("s2"_s); //直接设置当前状态为s2
sm.process_event(e3{});
assert(sm.is(X));
assert(fake_data.value == 42);
return 0;
}
日志
[Boost::ext].SML
还为我们提供了日志功能让我们更方便的跟踪和debug。
在命名空间中创建一个日志打印结构体
struct printf_logger {
template <class SM, class TEvent>
void log_process_event(const TEvent&) {
printf("[%s][process_event] %s\n", sml::aux::get_type_name<SM>(), sml::aux::get_type_name<TEvent>());
}
template <class SM, class TGuard, class TEvent>
void log_guard(const TGuard&, const TEvent&, bool result) {
printf("[%s][guard] %s %s %s\n", sml::aux::get_type_name<SM>(), sml::aux::get_type_name<TEvent>(),
sml::aux::get_type_name<TGuard>(), (result ? "[OK]" : "[Reject]"));
}
template <class SM, class TAction, class TEvent>
void log_action(const TAction&, const TEvent&) {
printf("[%s][action] %s %s\n", sml::aux::get_type_name<SM>(), sml::aux::get_type_name<TEvent>(),
sml::aux::get_type_name<TAction>());
}
template <class SM, class TSrcState, class TDstState>
void log_state_change(const TSrcState& src, const TDstState& dst) {
printf("[%s][transition] %s -> %s\n", sml::aux::get_type_name<SM>(), src.c_str(), dst.c_str());
}
};
在创建状态机时载入即可
printf_logger log{};
sm<System, logger<printf_logger>, defer_queue<std::deque>, dispatch<back::policies::switch_stm>> system{log};
Policies
[Boost::ext].SML
为我们提供了四种dispatch的实现方式,分别是jump table,switch/case,if/else和fold expression,我们可以在创建状态机时指定其中的任意一个。
sml::sm<Connection, sml::dispatch<sml::back::policies::jump_table>> connection{};
sml::sm<Connection, sml::dispatch<sml::back::policies::switch_stm>> connection{};
sml::sm<Connection, sml::dispatch<sml::back::policies::branch_stm>> connection{};
sml::sm<Connection, sml::dispatch<sml::back::policies::fold_expr>> connection{};
支持线程保护和日志记录。
sml::sm<example, sml::thread_safe<std::recursive_mutex>> sm; // thread safe policy
sml::sm<example, sml::logger<my_logger>> sm; // logger policy
sml::sm<example, sml::thread_safe<std::recursive_mutex>, sml::logger<my_logger>> sm; // thread safe and logger policy
sml::sm<example, sml::logger<my_logger>, sml::thread_safe<std::recursive_mutex>> sm; // thread safe and logger policy
实例

img
这是一个能够处理外部请求的系统,系统开启后能够处理外来的连接,挂起时能保留外部请求并在恢复后重新处理,同时,它还有一个单独的分支(watchdog定时器)用于监视系统正常运行。
#include <boost/sml.hpp>
#include <boost/sml/utility/dispatch_table.hpp>
#include <cassert>
#include <fstream>
#include <cstdio>
#include <cstdlib>
#include <queue>
#include <iostream>
namespace sml = boost::sml;
namespace {
//logger
struct printf_logger {
template<class SM, class TEvent> void log_process_event(const TEvent&)
{
printf("[%s][process_event] %s\n", sml::aux::get_type_name<SM>(), sml::aux::get_type_name<TEvent>());
}
template<class SM, class TGuard, class TEvent> void log_guard(const TGuard&, const TEvent&,bool result)
{
printf("[%s][guard] %s %s %s\n", sml::aux::get_type_name<SM>(), sml::aux::get_type_name<TEvent>(),
sml::aux::get_type_name<TGuard>(), (result ? "[OK]" : "[Reject]"));
}
template<class SM, class TAction, class TEvent> void log_action(const TAction, const TEvent&)
{
printf("[%s][action] %s %s\n", sml::aux::get_type_name<SM>(), sml::aux::get_type_name<TEvent>(),
sml::aux::get_type_name<TAction>());
}
template <class SM, class TSrcState, class TDstState>
void log_state_change(const TSrcState& src, const TDstState& dst) {
printf("[%s][transition] %s -> %s\n", sml::aux::get_type_name<SM>(), src.c_str(), dst.c_str());
}
};
// events
struct connect : sml::utility::id<__COUNTER__> {};
struct established : sml::utility::id<__COUNTER__> {};
struct ping : sml::utility::id<__COUNTER__> {
explicit ping(const void* msg)
: valid{msg != nullptr}{}
bool valid{};
};
struct timeout : sml::utility::id<__COUNTER__> {};
struct disconnect : sml::utility::id<__COUNTER__> {};
struct power_up : sml::utility::id<__COUNTER__> {};
struct suspend : sml::utility::id<__COUNTER__> {};
struct resume : sml::utility::id<__COUNTER__> {};
struct tick : sml::utility::id<__COUNTER__> {};
// guards
const auto is_valid = [](const auto& event) {
std::puts("is_valid");
return event.valid;
};
const auto has_battery = []() {
std::puts("has_battery");
return true;
};
const auto is_healthy = []() {
std::puts("is_healthy");
return true;
};
// actions
const auto setup = []() {std::cout << "setup\n"; };
const auto establish = []() {std::cout << "establish\n"; };
const auto resetTimeout = []() {std::cout << "resetTimeout\n"; };
const auto defer = []() {std::cout << "defer\n"; };
const auto close = []() {std::cout << "close\n"; };
class System
{
// status
class Idle;
class Disconnected;
class Connecting;
class Connected;
class Suspended;
class Watchdog;
struct Connection {
auto operator()() const {
using namespace sml;
return make_transition_table(
*state<disconnect> +event<connect> / establish = state<Connecting>,
state<Connecting>+event<established> = state<Connected>,
state<Connected>+event<timeout> = state<Connecting>,
state<Connected>+event<ping>[is_valid] / resetTimeout,
state<Connected>+event<disconnect> = state<Disconnected>
);
};
};
public:
auto operator()() const {
using namespace sml;
return make_transition_table(
*state<Idle> +event<power_up>[has_battery and is_healthy] / setup = state<Connection>,
state<Connection> +event<suspend> = state<Suspended>,
state<Suspended> +event<ping> / defer,
state<Suspended> +event<resume> = state<Connection>,
//
*state<Watchdog> +event<tick> / resetTimeout,
state<Watchdog> +event<timeout> = X
);
};
};
}// namespace
int main(int argc,char**argv) {
using namespace sml;
printf_logger log{};
sm<System, logger<printf_logger>, defer_queue<std::deque>, dispatch<back::policies::switch_stm>> system{ log };
auto dispatch = sml::utility::make_dispatch_table<void*, 0, __COUNTER__ - 1>(system);
for (auto i = 1; i < argc; i++) {
const auto event_id = std::atoi(argv[i]);
dispatch(nullptr, event_id);
}
return 0;
}
开发思路
状态机整体开发思路围绕着状态转移来进行。主要是为了替代(减少)多线程的使用,避免线程混乱的情况。
主要分为两个方面:
- 对系统的 【事件】、【状态】、【行为】 进行抽象。
- 明确【状态转移】的路径。
在某一动作执行前中后,都可以看作一个 事件 ,当事件发生时,系统的状态就可能发生 转移,根据是否满足【guard】条件,触发对应行为,并进行状态转移。