Boost C++ 库

...是世界上最受推崇、设计最精良的 C++ 库项目之一。 Herb SutterAndrei Alexandrescu, C++ Coding Standards

Boost Statechart 库 - FAQ - Boost C++ 函数库

Boost Statechart 库

常见问题解答 (FAQ)

状态局部存储有什么好处?
如何隐藏状态机的内部工作细节,不让客户端知道?
能否从给定的状态机继承,并在子类中修改其布局?
UML2.0 特性怎么样?
为什么在状态析构函数中访问状态机时会出现断言?
Boost.Statechart 适合嵌入式应用吗?
你们的库适合有硬实时要求的应用吗?
使用模板化状态时,出现 'inner_context_type' 未定义的错误。是什么问题?
我的编译器报告库代码有错误。这是 Boost.Statechart 的 bug 吗?
能否在运行时禁用某个状态的历史记录?
如何将状态机编译成动态链接库 (DLL)?
Boost.Statechart 支持多态事件吗?
为什么在使用多重继承时,退出动作的调用顺序是错误的?

状态局部存储有什么好处?

这最好用一个例子来解释

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 来说可能更容易阅读,但它确实存在一些原版代码中没有的问题。

在像这样的小例子中,这些都不是大问题,可以在单个翻译单元中由单个程序员轻松实现。然而,对于分散在多个翻译单元中、甚至可能由不同程序员维护的大型复杂状态机,这些问题会迅速成为一个主要问题。

如何隐藏状态机的内部工作细节,不让客户端知道?

要理解为什么以及如何实现这一点,回忆以下事实很重要

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 >();
  }
};

UML 2.0 特性怎么样?

该库是在 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(); }

Boost.Statechart 适合嵌入式应用吗?

这取决于。正如在性能页面上的 速度与可伸缩性权衡 中所述,该库提供的几乎无限的可伸缩性是有代价的。尤其是小型和简单的 FSM,可以很容易地实现,以消耗更少的周期、更少的内存,并在可执行文件中占用更少的代码空间。以下是一些明显非常粗略的估计。

如上所述,这些是非常粗略的估计,源于在台式电脑上使用该库的经验,因此它们仅应用于决定是否值得在您的目标平台上进行自己的性能测试。

你们的库适合有硬实时要求的应用吗?

是的。开箱即用,该库执行的唯一可能具有非确定性时间的 are calls to std::allocator<> member functions and dynamic_casts. 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_casts 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。)

使用模板化状态时,出现 'inner_context_type' 未定义的错误。是什么问题?

以下代码会生成此类错误

#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;
}

如果将模板 AB 替换为普通类型,则上述代码可以编译而不会出错。这是因为 C++ 将前向声明的模板与前向声明的类型处理方式不同。具体来说,编译器会在模板尚未定义时尝试访问 B< X > 的成员 typedef。幸运的是,通过将所有内部初始状态参数放入 mpl::list<> 中,可以轻松避免此问题,如下所示。

struct A : sc::simple_state<
  A< X >, Machine, mpl::list< B< X > > > {};

有关技术细节,请参阅 此帖子

我的编译器报告库代码有错误。这是 Boost.Statechart 的 bug 吗?

可能不会。出现此类编译时错误有多种可能的原因。

  1. 您的编译器太有 bug,无法编译该库,请参阅 此处 了解您编译器的状态。如果您必须在项目中使用这样的编译器,我恐怕 Boost.Statechart 不适合您。
  2. 错误报告的行类似于以下内容
    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 行找到了罪魁祸首。
  3. 错误报告的行与 BOOST_STATIC_ASSERT 附近无关。使用第 2 点中描述的技术来查看是哪一行代码导致了问题。如果您的代码是正确的,那么您就发现了编译器或 Boost.Statechart 中的 bug。请 发送给我 一个小的但完整的程序来说明问题。谢谢!

能否在运行时禁用某个状态的历史记录?

是的,请参阅 simple_state::clear_shallow_history()simple_state::clear_deep_history()。调用这些函数通常比引入额外的普通转换更可取,当……

如何将状态机编译成动态链接库 (DLL)?

对用户不可见,该库使用静态数据成员来实现其自己的、速度优化的 RTTI 机制,用于 event<>simple_state<> 子类型。每当在头文件中定义这样的子类型,然后将其包含在多个 TU 中时,链接器随后需要消除静态数据成员的重复定义。只要所有这些 TU 都被静态链接到同一个二进制文件中,这通常都能顺利进行。当涉及 DLL 时,情况要复杂得多。TuTest*.?pp 文件说明了这一点。

没有任何预防措施(例如,MSVC 兼容编译器上的 __declspec(dllexport)),在大多数平台上,两个二进制文件(exe 和 dll)现在都包含自己的静态数据成员实例。由于 RTTI 机制假定运行时存在该成员的唯一对象,因此当运行 exe 的进程也加载 dll 时,该机制会发生灾难性失败。不同的平台对这个问题有不同的处理方式。

Boost.Statechart 支持多态事件吗?

否。虽然事件可以相互派生,以避免重复编写通用代码,但 响应 只能为最派生的事件定义。

示例

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

也就是说,OuterEntryExitDisplayer 基类部分在其 Inner 部分之前被析构,尽管 Inner::~Inner()Outer::~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<> 成为状态的第一个基类。这确保了附加基类的析构函数在状态基类析构函数所使用的递归改变析构顺序之前被调用。


Valid HTML 4.01 Transitional

修订于 2008 年 1 月 5 日

版权 © 2003-2008 Andreas Huber Dönni

根据Boost软件许可证版本1.0分发。(请参阅随附文件LICENSE_1_0.txt或复制自https://boost.ac.cn/LICENSE_1_0.txt