Boost Statechart 库常见问题解答 (FAQ) |
这最好用一个例子来解释
struct Active; struct Stopped; struct Running; struct StopWatch : sc::state_machine< StopWatch, Active > { // startTime_ remains uninitialized, because there is no reasonable default StopWatch() : elapsedTime_( 0.0 ) {} ~StopWatch() { terminate(); } double ElapsedTime() const { // Ugly switch over the current state. if ( state_cast< const Stopped * >() != 0 ) { return elapsedTime_; } else if ( state_cast< const Running * >() != 0 ) { return elapsedTime_ + std::difftime( std::time( 0 ), startTime_ ); } else // we're terminated { throw std::bad_cast(); } } // elapsedTime_ is only meaningful when the machine is not terminated double elapsedTime_; // startTime_ is only meaningful when the machine is in Running std::time_t startTime_; }; struct Active : sc::state< Active, StopWatch, Stopped > { typedef sc::transition< EvReset, Active > reactions; Active( my_context ctx ) : my_base( ctx ) { outermost_context().elapsedTime_ = 0.0; } }; struct Running : sc::state< Running, Active > { typedef sc::transition< EvStartStop, Stopped > reactions; Running( my_context ctx ) : my_base( ctx ) { outermost_context().startTime_ = std::time( 0 ); } ~Running() { outermost_context().elapsedTime_ += std::difftime( std::time( 0 ), outermost_context().startTime_ ); } }; struct Stopped : sc::simple_state< Stopped, Active > { typedef sc::transition< EvStartStop, Running > reactions; };
这个 StopWatch 在实现与 教程中的 StopWatch 相同的行为时,并没有使用状态局部存储。虽然这段代码对于 untrained eye 来说可能更容易阅读,但它确实存在一些原版代码中没有的问题。
StopWatch::ElapsedTime()
)。这本质上是复制了 FSM 的一些状态逻辑。因此,每当我们想改变状态机的布局时,很可能也需要改变这个丑陋的 switch。更糟糕的是,如果我们忘记更改 switch,代码可能仍然可以编译,甚至可能默默地执行错误的操作。请注意,这在教程版本中是不可能发生的,教程版本至少会抛出异常,或者干脆拒绝编译。此外,对于教程中的 StopWatch,程序员第一次就正确更改代码的可能性要大得多,因为计算经过时间的代码与更新变量的代码位于接近的位置。state_machine<>
子类因此会成为一个更改热点,这几乎是糟糕设计的肯定指标。在像这样的小例子中,这些都不是大问题,可以在单个翻译单元中由单个程序员轻松实现。然而,对于分散在多个翻译单元中、甚至可能由不同程序员维护的大型复杂状态机,这些问题会迅速成为一个主要问题。
要理解为什么以及如何实现这一点,回忆以下事实很重要
sc::state_machine
的 InitialState
模板参数可以是一个不完整的类型(即前向声明)。state_machine<>::initiate()
的类模板成员函数会创建一个初始状态的对象。因此,在编译器到达调用 initiate()
的点之前,必须知道该状态的定义。为了能够将状态机的初始状态隐藏在 .cpp 文件中,我们因此必须不再让客户端调用 initiate()
。相反,我们在 .cpp 文件中调用它,在知道初始状态完整定义的地方。
示例
StopWatch.hpp
// define events ... struct Active; // the only visible forward struct StopWatch : sc::state_machine< StopWatch, Active > { StopWatch(); };
StopWatch.cpp
struct Stopped; struct Active : sc::simple_state< Active, StopWatch, Stopped > { typedef sc::transition< EvReset, Active > reactions; }; struct Running : sc::simple_state< Running, Active > { typedef sc::transition< EvStartStop, Stopped > reactions; }; struct Stopped : sc::simple_state< Stopped, Active > { typedef sc::transition< EvStartStop, Running > reactions; }; StopWatch::StopWatch() { // For example, we might want to ensure that the state // machine is already started after construction. // Alternatively, we could add our own initiate() function // to StopWatch and call the base class initiate() in the // implementation. initiate(); }
PingPong 示例演示了如何隐藏异步状态机 (asynchronous_state_machine<>
) 子类的内部工作细节。
是的,但与某些 FSM 代码生成器允许的不同,Boost.Statechart 状态机只能以基状态机设计者预见的方式执行此操作。
struct EvStart : sc::event< EvStart > {}; struct Idle; struct PumpBase : sc::state_machine< PumpBase, Idle > { virtual sc::result react( Idle & idle, const EvStart & ) const; }; struct Idle : sc::simple_state< Idle, PumpBase > { typedef sc::custom_reaction< EvStart > reactions; sc::result react( const EvStart & evt ) { return context< PumpBase >().react( *this, evt ); } }; struct Running : sc::simple_state< Running, PumpBase > {}; sc::result PumpBase::react( Idle & idle, const EvStart & ) const { return idle.transit< Running >(); } struct MyRunning : sc::simple_state< MyRunning, PumpBase > {}; struct MyPump : PumpBase { virtual sc::result react( Idle & idle, const EvStart & ) const { return idle.transit< MyRunning >(); } };
该库是在 2.0 出现之前设计的。因此,除非另有明确说明,否则该库实现了 UML1.5 标准所规定的行为。以下是 2.0 语义与 Boost.Statechart 语义之间差异的不完整列表。
当使用 NDEBUG
编译(未定义)时,运行以下程序会导致断言失败。
#include <boost/statechart/state_machine.hpp> #include <boost/statechart/simple_state.hpp> #include <iostream> struct Initial; struct Machine : boost::statechart::state_machine< Machine, Initial > { Machine() { someMember_ = 42; } int someMember_; }; struct Initial : boost::statechart::simple_state< Initial, Machine > { ~Initial() { std::cout << outermost_context().someMember_; } }; int main() { Machine().initiate(); return 0; }
问题出现的原因是 state_machine<>::~state_machine
不可避免地会析构所有剩余的活动状态。此时,Machine::~Machine
已经运行完毕,因此不允许访问任何 Machine
成员。通过定义以下析构函数可以避免此问题。
~Machine() { terminate(); }
这取决于。正如在性能页面上的 速度与可伸缩性权衡 中所述,该库提供的几乎无限的可伸缩性是有代价的。尤其是小型和简单的 FSM,可以很容易地实现,以消耗更少的周期、更少的内存,并在可执行文件中占用更少的代码空间。以下是一些明显非常粗略的估计。
state_machine<>::process_event()
中的花费不应超过 1000 个周期。对于更复杂的状态机,处理单个事件的最坏情况时间大致与同时活动状态的数量成线性比例关系,而典型平均值远低于此。因此,一个最多有 10 个同时活动状态、运行在 100MHz CPU 上的相当复杂的状态机,应该能够每秒处理超过 10,000 个事件。如上所述,这些是非常粗略的估计,源于在台式电脑上使用该库的经验,因此它们仅应用于决定是否值得在您的目标平台上进行自己的性能测试。
是的。开箱即用,该库执行的唯一可能具有非确定性时间的 are calls to std::allocator<>
member functions and dynamic_cast
s. std::allocator<>
member function calls can be avoided by passing a custom allocator to event<>
, state_machine<>
, asynchronous_state_machine<>
, fifo_scheduler<>
and fifo_worker<>
. dynamic_cast
s can be avoided by not calling the state_cast<>
member functions of state_machine<>
, simple_state<>
and state<>
but using the deterministic variant state_downcast<>
instead.(通过将自定义分配器传递给 event<>
、state_machine<>
、asynchronous_state_machine<>
、fifo_scheduler<>
和 fifo_worker<>
可以避免调用 std::allocator<>
成员函数。通过不调用 state_machine<>
、simple_state<>
和 state<>
的 state_cast<>
成员函数,而是使用确定性变体 state_downcast<>
来避免 dynamic_cast
。)
以下代码会生成此类错误
#include <boost/statechart/state_machine.hpp> #include <boost/statechart/simple_state.hpp> namespace sc = boost::statechart; template< typename X > struct A; struct Machine : sc::state_machine< Machine, A< int > > {}; template< typename X > struct B; template< typename X > struct A : sc::simple_state< A< X >, Machine, B< X > > {}; template< typename X > struct B : sc::simple_state< B< X >, A< X > > {}; int main() { Machine machine; machine.initiate(); return 0; }
如果将模板 A
和 B
替换为普通类型,则上述代码可以编译而不会出错。这是因为 C++ 将前向声明的模板与前向声明的类型处理方式不同。具体来说,编译器会在模板尚未定义时尝试访问 B< X >
的成员 typedef。幸运的是,通过将所有内部初始状态参数放入 mpl::list<>
中,可以轻松避免此问题,如下所示。
struct A : sc::simple_state< A< X >, Machine, mpl::list< B< X > > > {};
有关技术细节,请参阅 此帖子。
可能不会。出现此类编译时错误有多种可能的原因。
BOOST_STATIC_ASSERT( ( mpl::less< orthogonal_position, typename context_type::no_of_orthogonal_regions >::value ) );很可能,您的代码有错误。该库有许多此类编译时断言,以确保无效的状态机无法被编译(有关编译时报告的错误类型,请参阅编译失败测试)。在这些断言的上方,都有注释解释了问题。在几乎所有当前编译器上,模板代码中的错误都会伴随当前的“实例化堆栈”。这非常类似于您在调试器中看到的调用堆栈,“实例化堆栈”允许您追溯错误,穿过库代码的实例化,直到您找到导致问题的代码行。例如,这是 InconsistentHistoryTest1.cpp 中代码的 MSVC7.1 错误消息。
...\boost\statechart\shallow_history.hpp(34) : error C2027: use of undefined type 'boost::STATIC_ASSERTION_FAILURE<x>' with [ x=false ] ...\boost\statechart\shallow_history.hpp(34) : see reference to class template instantiation 'boost::STATIC_ASSERTION_FAILURE<x>' being compiled with [ x=false ] ...\boost\statechart\simple_state.hpp(861) : see reference to class template instantiation 'boost::statechart::shallow_history<DefaultState>' being compiled with [ DefaultState=B ] ...\boost\statechart\simple_state.hpp(599) : see reference to function template instantiation 'void boost::statechart::simple_state<MostDerived,Context,InnerInitial>::deep_construct_inner_impl_non_empty::deep_construct_inner_impl<InnerList>(const boost::statechart::simple_state<MostDerived,Context,InnerInitial>::inner_context_ptr_type &,boost::statechart::simple_state<MostDerived,Context,InnerInitial>::outermost_context_base_type &)' being compiled with [ MostDerived=A, Context=InconsistentHistoryTest, InnerInitial=boost::mpl::list<boost::statechart::shallow_history<B>>, InnerList=boost::statechart::simple_state<A,InconsistentHistoryTest,boost::mpl::list<boost::statechart::shallow_history<B>>>::inner_initial_list ] ...\boost\statechart\simple_state.hpp(567) : see reference to function template instantiation 'void boost::statechart::simple_state<MostDerived,Context,InnerInitial>::deep_construct_inner<boost::statechart::simple_state<MostDerived,Context,InnerInitial>::inner_initial_list>(const boost::statechart::simple_state<MostDerived,Context,InnerInitial>::inner_context_ptr_type &,boost::statechart::simple_state<MostDerived,Context,InnerInitial>::outermost_context_base_type &)' being compiled with [ MostDerived=A, Context=InconsistentHistoryTest, InnerInitial=boost::mpl::list<boost::statechart::shallow_history<B>> ] ...\boost\statechart\simple_state.hpp(563) : while compiling class-template member function 'void boost::statechart::simple_state<MostDerived,Context,InnerInitial>::deep_construct(const boost::statechart::simple_state<MostDerived,Context,InnerInitial>::context_ptr_type & ,boost::statechart::simple_state<MostDerived,Context,InnerInitial>::outermost_context_base_type &)' with [ MostDerived=A, Context=InconsistentHistoryTest, InnerInitial=boost::mpl::list<boost::statechart::shallow_history<B>> ] ...\libs\statechart\test\InconsistentHistoryTest1.cpp(29) : see reference to class template instantiation 'boost::statechart::simple_state<MostDerived,Context,InnerInitial>' being compiled with [ MostDerived=A, Context=InconsistentHistoryTest, InnerInitial=boost::mpl::list<boost::statechart::shallow_history<B>> ]根据您使用的 IDE,您可能需要切换到另一个窗口才能看到完整的错误消息(例如,对于 Visual Studio 2003,您需要切换到“输出”窗口)。从顶部开始,向下查看实例化列表,您会发现每个实例化都附带文件名和行号。忽略所有属于库的文件,我们在文件 InconsistentHistoryTest1.cpp 的底部附近,第 29 行找到了罪魁祸首。
是的,请参阅 simple_state::clear_shallow_history() 和 simple_state::clear_deep_history()。调用这些函数通常比引入额外的普通转换更可取,当……
对用户不可见,该库使用静态数据成员来实现其自己的、速度优化的 RTTI 机制,用于 event<>
和 simple_state<>
子类型。每当在头文件中定义这样的子类型,然后将其包含在多个 TU 中时,链接器随后需要消除静态数据成员的重复定义。只要所有这些 TU 都被静态链接到同一个二进制文件中,这通常都能顺利进行。当涉及 DLL 时,情况要复杂得多。TuTest*.?pp 文件说明了这一点。
没有任何预防措施(例如,MSVC 兼容编译器上的 __declspec(dllexport)
),在大多数平台上,两个二进制文件(exe 和 dll)现在都包含自己的静态数据成员实例。由于 RTTI 机制假定运行时存在该成员的唯一对象,因此当运行 exe 的进程也加载 dll 时,该机制会发生灾难性失败。不同的平台对这个问题有不同的处理方式。
__declspec(dllimport)
和 __declspec(dllexport)
,它们允许精确定义需要从 DLL 加载的内容(有关如何执行此操作的示例,请参见 TuTest)。因此,可以使用内部 RTTI 机制,但必须小心正确地导出和导入所有定义在被编译到多个二进制文件中的头文件中的 event<>
和 simple_state<>
子类型。或者,当然也可以使用 BOOST_STATECHART_USE_NATIVE_RTTI 来避免导入和导出的工作。否。虽然事件可以相互派生,以避免重复编写通用代码,但 响应 只能为最派生的事件定义。
示例
template< class MostDerived > struct EvButtonPressed : sc::event< MostDerived > { // common code }; struct EvPlayButtonPressed : EvButtonPressed< EvPlayButtonPressed > {}; struct EvStopButtonPressed : EvButtonPressed< EvStopButtonPressed > {}; struct EvForwardButtonPressed : EvButtonPressed< EvForwardButtonPressed > {}; /* ... */ // We want to turn the player on, no matter what button we // press in the Off state. Although we can write the reaction // code only once, we must mention all most-derived events in // the reaction list. struct Off : sc::simple_state< Off, Mp3Player > { typedef mpl::list< sc::custom_reaction< EvPlayButtonPressed >, sc::custom_reaction< EvStopButtonPressed >, sc::custom_reaction< EvForwardButtonPressed > > reactions; template< class MostDerived > sc::result react( const EvButtonPressed< MostDerived > & ) { // ... } };
更新:这方面的实现已经有了很大的变化。在罕见的情况下(当动作在具有正交区域的状态机中传播异常,并且状态图布局满足某些条件时),仍然可能出现此行为,但无法再用下面的示例程序演示。但是,所描述的解决方法仍然有效,并确保此行为永远不会出现。
对于 simple_state<>
和 state<>
子类型,它们的析构函数肯定不是按此顺序执行的,但附加基类的析构函数可能会按构造顺序(而不是反向构造顺序)调用。
#include <boost/statechart/state_machine.hpp> #include <boost/statechart/simple_state.hpp> namespace sc = boost::statechart; class EntryExitDisplayer { protected: EntryExitDisplayer( const char * pName ) : pName_( pName ) { std::cout << pName_ << " entered\n"; } ~EntryExitDisplayer() { std::cout << pName_ << " exited\n"; } private: const char * const pName_; }; struct Outer; struct Machine : sc::state_machine< Machine, Outer > {}; struct Inner; struct Outer : EntryExitDisplayer, sc::simple_state< Outer, Machine, Inner > { Outer() : EntryExitDisplayer( "Outer" ) {} }; struct Inner : EntryExitDisplayer, sc::simple_state< Inner, Outer > { Inner() : EntryExitDisplayer( "Inner" ) {} }; int main() { Machine myMachine; myMachine.initiate(); return 0; }
此程序将产生以下输出
Outer entered Inner entered Outer exited Inner exited
也就是说,Outer
的EntryExitDisplayer
基类部分在其 Inner
部分之前被析构,尽管 Inner::~Inner()
在 Outer::~Outer()
之前被调用。这种有些违反直觉的行为是由以下事实引起的。
Inner
的 simple_state<>
基类部分负责析构 Outer
。因此,当调用 Outer
析构函数时,调用堆栈如下所示。
Outer::~Outer() simple_state< Inner, ... >::~simple_state() Inner::~Inner()
请注意,Inner::~Inner()
还没有机会析构其 EntryExitDisplayer
基类部分,因为它首先必须调用第二个基类的析构函数。现在,Outer::~Outer()
将首先析构其 simple_state< Outer, ... >
基类部分,然后对其 EntryExitDisplayer
基类部分执行相同的操作。堆栈随后回溯到 Inner::~Inner()
,它最终可以调用 EntryExitDisplayer::~EntryExitDisplayer()
。
幸运的是,有一个简单的解决方法:始终让 simple_state<>
和 state<>
成为状态的第一个基类。这确保了附加基类的析构函数在状态基类析构函数所使用的递归改变析构顺序之前被调用。
修订于 2008 年 1 月 5 日
版权 © 2003-2008 Andreas Huber Dönni
根据Boost软件许可证版本1.0分发。(请参阅随附文件LICENSE_1_0.txt或复制自https://boost.ac.cn/LICENSE_1_0.txt)