文章目录
- 概述
- 为什么要迁移到 C++,以及 C++ 的陷阱
- 目标与挑战
- 为什么不能直接使用 `std::function`?
- 解决方案:POD 回调与模板 Trampoline
- 核心设计
- 模板 trampoline 实现
- 两种成员函数绑定策略
- 1. **Per-Transition Context(每个状态转移绑定一个对象)**
- 2. **`sm->user_data`(通过状态机获取对象)**
- 性能对比
- 附完整代码
概述
本文将探讨如何将一个 C 风格的 HSM 迁移至 C++14/17,并解决在这一过程中遇到的核心挑战:如何在不引入堆分配、不牺牲实时性的前提下,优雅地将成员函数作为回调?
为什么要迁移到 C++,以及 C++ 的陷阱
目标与挑战
我们希望用 C++ 重构一个 C 风格的 HSM,以实现以下目标:
- 更好的接口与可读性:利用面向对象特性,将状态机逻辑与业务对象紧密结合。
- 零堆分配与确定性:保留在嵌入式/RTOS(如 RT-Thread)上无堆分配、可静态初始化和确定性行为的优势。
- 简化成员函数绑定:方便地将成员函数作为状态机的回调,而无需为每个方法手动编写静态封装函数。
为什么不能直接使用 std::function
?
在通用编程中,std::function
提供了极大的便利,它能以统一的方式存储各种可调用对象。然而,在受限的嵌入式环境中,它可能带来致命的缺点:
- 潜在的堆分配:
std::function
通常利用小对象优化(Small Object Optimization, SOO)来避免小尺寸可调用对象的堆分配。但当可调用对象超过其内部缓冲区大小时,它会回退到堆分配。 - 不可预测性:堆分配操作(
new
/malloc
)会引入不确定的延迟抖动,这在实时系统中是不可接受的。 - 无法静态初始化:
std::function
不能在编译时被定义为constexpr
,这意味着你无法将其直接放入 ROM 表(如.rodata
段),从而增加了 RAM 占用。 - 更大的代码体积:
std::function
的类型擦除机制和复杂的实现路径会显著增加二进制文件的大小。
因此,在状态机的热路径(如 dispatch
、action
或 guard
回调)中,应坚决避免 std::function
。
解决方案:POD 回调与模板 Trampoline
为了解决 std::function
的问题,我们提出一种基于**(Plain Old Data, POD)**的回调方案。
核心设计
我们定义一个非常简单且轻量级的回调结构体:
// 核心数据结构:精简且为 POD
struct ActionCallback {using Fn = void(*)(void* ctx, StateMachine* sm, const Event* ev);void* ctx;Fn fn;
};struct GuardCallback {using Fn = bool(*)(void* ctx, StateMachine* sm, const Event* ev);void* ctx;Fn fn;
};// 状态转移表保持 POD
struct Transition {uint32_t event_id;const State* target;GuardCallback guard;ActionCallback action;TransitionType type;
};
该方案的核心优势在于:
- POD 类型:
ActionCallback
和GuardCallback
都是 POD 类型,可以被static const
定义并存储在 ROM 中,实现零运行时分配。 - 简洁的调用路径:调用仅包含一次上下文指针读取和一次函数指针的间接调用,性能开销低且可预测。
- 灵活的绑定:我们可以使用 C++ 模板来生成“trampoline”函数,将任意成员函数绑定到这个通用的
(void*, ...)
签名上。
模板 trampoline 实现
Trampoline(意为“跳板”)是一个内联的模板函数,它负责将通用的 (void*, ...)
参数转换为特定成员函数所需的 (this*, ...)
参数。
template<typename T, void (T::*MF)(StateMachine*, const Event*)>
inline void action_trampoline(void* ctx, StateMachine* sm, const Event* ev) {// 关键步骤:static_cast 将 void* 转换回目标对象指针T* obj = static_cast<T*>(ctx);// 成员函数调用(obj->*MF)(sm, ev);
}
通过这个 trampoline,我们可以方便地创建绑定,让状态机能够调用某个对象实例的特定成员函数:
template<typename T, void (T::*MF)(StateMachine*, const Event*)>
inline ActionCallback make_action(T* obj) {return { static_cast<void*>(obj), &action_trampoline<T, MF> };
}
static_cast
的开销误区:
很多人担心 static_cast<void*> -> T*
会引入额外的运行时开销。事实上,在绝大多数主流的 ABI(如 ARM、x86)和优化级别下,static_cast
是一个纯编译时指示,不会产生任何运行时代码。真正的开销来自于随后的间接调用。因此,不必担心 static_cast
会影响性能。
两种成员函数绑定策略
这套方案提供了两种实用的绑定策略,以适应不同的设计需求。
1. Per-Transition Context(每个状态转移绑定一个对象)
这种策略将上下文(ctx
)指针直接存储在 Transition
结构中。
- 优点:非常灵活,同一张状态转移表可以在运行时被多个对象实例复用,你只需在初始化时设置好每个回调的
ctx
指针。 - 适用场景:当一个状态表被多个不同实例共享,但每个实例的回调逻辑(即成员函数)是其自身状态的一部分时。
2. sm->user_data
(通过状态机获取对象)
该策略将 Transition
表设计为完全静态且无上下文指针(ctx = nullptr
)。在 trampoline 函数中,我们从 StateMachine
实例中预设的 user_data
字段获取目标对象。
- 优点:状态转移表可以被定义为
static constexpr
,完全存储在 ROM 中,不占用任何 RAM。这是性能和资源占用的最优解。 - 适用场景:每个状态机实例都唯一对应一个业务对象(例如在 Active Object 或 Actor 模式中)。你只需在初始化时通过
sm.set_user_data(this)
绑定一次即可。
性能对比
我们将几种常见的回调方案进行性能与开销的维度对比。
方案 | 内存开销 | 调用开销 | 静态初始化 | 动态绑定 |
---|---|---|---|---|
直接调用 | 低 | 极低(可内联) | N/A | 不支持 |
POD 回调 | 极低(POD) | 1次加载 + 1次间接调用 | 是 | 是 |
pointer-to-member | 低(与ABI相关) | 1次加载 + 1次间接调用 | 是 | 是 |
virtual | 低(vptr) | 1次间接调用 | N/A | 是 |
std::function | 可变(有堆分配) | 可变(复杂) | 否 | 是 |
- POD 回调:在可静态化、无堆分配与低开销之间取得了最佳平衡,是嵌入式场景下的首选。
pointer-to-member
:如果所有回调都属于同一类型(例如,所有action
回调都来自同一个MyFSM
类),pointer-to-member
可以进一步优化,提供与 POD 回调相似甚至更低的开销。但其在多继承或多类型混用时会变得复杂。std::function
:仅在初始化、非关键后台任务或非实时逻辑中使用。严禁在中断服务例程(ISR)或任何高频热路径中使用。
附完整代码
/*state_machine.hppModern C++14/17 hierarchical state machine (HSM) implementation.
*/
#ifndef STATE_MACHINE_HPP
#define STATE_MACHINE_HPP#include <cstdint>
#include <cstddef>
#include <cassert>
#include <type_traits>#ifndef HSM_ASSERT
#define HSM_ASSERT(expr) assert(expr)
#endifnamespace hsm
{using uint32_t = std::uint32_t;
using uint8_t = std::uint8_t;
using size_t = std::size_t;
using bool_t = bool;// Forward
struct State;
struct Transition;
struct Event;
class StateMachine;/* Event */
struct Event
{uint32_t id;void *context;
};/* TransitionType */
enum class TransitionType : uint8_t
{External = 0,Internal = 1
};/* POD callback descriptors (no allocations, can be static) */
struct ActionCallback {using Fn = void (*)(void* ctx, StateMachine* sm, const Event* ev);void* ctx;Fn fn;
};struct GuardCallback {using Fn = bool (*)(void* ctx, const StateMachine* sm, const Event* ev);void* ctx;Fn fn;
};/* No-op constants */
inline void action_noop(void* /*ctx*/, StateMachine* /*sm*/, const Event* /*ev*/) {}
inline bool guard_always_true(void* /*ctx*/, const StateMachine* /*sm*/, const Event* /*ev*/) { return true; }namespace detail {/* Member function trampoline for actions */
template<typename T, void (T::*MF)(StateMachine*, const Event*)>
inline void action_trampoline(void* ctx, StateMachine* sm, const Event* ev) {T* obj = static_cast<T*>(ctx);(obj->*MF)(sm, ev);
}/* Member function trampoline for guards */
template<typename T, bool (T::*MG)(const StateMachine*, const Event*)>
inline bool guard_trampoline(void* ctx, const StateMachine* sm, const Event* ev) {T* obj = static_cast<T*>(ctx);return (obj->*MG)(sm, ev);
}/* Free/static function trampolines */
template<void (*F)(StateMachine*, const Event*)>
inline void action_fn_trampoline(void* /*ctx*/, StateMachine* sm, const Event* ev) {F(sm, ev);
}template<bool (*G)(const StateMachine*, const Event*)>
inline bool guard_fn_trampoline(void* /*ctx*/, const StateMachine* sm, const Event* ev) {return G(sm, ev);
}/* Forward declarations for trampolines needing StateMachine::user_data() */
template<typename T, void (T::*MF)(StateMachine*, const Event*)>
inline void action_from_sm_user_data(void* /*ctx*/, StateMachine* sm, const Event* ev);template<typename T, bool (T::*MG)(const StateMachine*, const Event*)>
inline bool guard_from_sm_user_data(void* /*ctx*/, const StateMachine* sm, const Event* ev);} // namespace detail/* Helper factories *//* Bind member function with explicit ctx pointer (per-transition ctx) */
template<typename T, void (T::*MF)(StateMachine*, const Event*)>
inline ActionCallback make_action(T* obj) {return ActionCallback{ static_cast<void*>(obj), &detail::action_trampoline<T, MF> };
}template<typename T, bool (T::*MG)(const StateMachine*, const Event*)>
inline GuardCallback make_guard(T* obj) {return GuardCallback{ static_cast<void*>(obj), &detail::guard_trampoline<T, MG> };
}template<void (*F)(StateMachine*, const Event*)>
inline ActionCallback make_action_fn() {return ActionCallback{ nullptr, &detail::action_fn_trampoline<F> };
}template<bool (*G)(const StateMachine*, const Event*)>
inline GuardCallback make_guard_fn() {return GuardCallback{ nullptr, &detail::guard_fn_trampoline<G> };
}template<typename T, void (T::*MF)(StateMachine*, const Event*)>
inline ActionCallback make_action_using_sm_user_data() {return ActionCallback{ nullptr, &detail::action_from_sm_user_data<T, MF> };
}template<typename T, bool (T::*MG)(const StateMachine*, const Event*)>
inline GuardCallback make_guard_using_sm_user_data() {return GuardCallback{ nullptr, &detail::guard_from_sm_user_data<T, MG> };
}/* Transition and State structures (POD-friendly) */
struct Transition
{uint32_t event_id;const State* target; // ignored for internal transitionsGuardCallback guard;ActionCallback action;TransitionType type;
};struct State
{const State* parent;ActionCallback entry_action;ActionCallback exit_action;const Transition* transitions;size_t num_transitions;const char* name;
};/* StateMachine class */
class StateMachine
{
public:StateMachine() noexcept;~StateMachine() noexcept = default;StateMachine(const StateMachine&) = delete;StateMachine& operator=(const StateMachine&) = delete;void init(const State* initial_state,const State** entry_path_buffer,uint8_t buffer_size,void* user_data = nullptr,ActionCallback unhandled_hook = ActionCallback{nullptr, nullptr}) noexcept;void deinit() noexcept;void reset() noexcept;bool dispatch(const Event* event) noexcept;bool is_in_state(const State* state) const noexcept;const char* get_current_state_name() const noexcept;void* user_data() const noexcept { return _user_data; }void set_user_data(void* p) noexcept { _user_data = p; }private:uint8_t _get_state_depth(const State* state) const noexcept;const State* _find_lca(const State* s1, const State* s2) const noexcept;void _perform_transition(const State* target_state, const Event* event) noexcept;const Transition* _find_matching_transition(const State* state, const Event* event, bool* guard_passed) const noexcept;bool _execute_transition(const Transition* transition, const Event* event) noexcept;bool _process_state_transitions(const State* state, const Event* event) noexcept;void _execute_exit_actions(const State* source_state, const State* lca, const Event* event) noexcept;int _build_entry_path(const State* target_state, const State* lca) noexcept;void _execute_entry_actions(uint8_t path_length, const Event* event) noexcept;private:const State* _current_state = nullptr;const State* _initial_state = nullptr;void* _user_data = nullptr;ActionCallback _unhandled_hook = ActionCallback{nullptr, nullptr};const State** _entry_path_buffer = nullptr;uint8_t _buffer_size = 0;
};/* ---------------- Implementation ---------------- */inline StateMachine::StateMachine() noexcept = default;inline void StateMachine::init(const State* initial_state,const State** entry_path_buffer,uint8_t buffer_size,void* user_data,ActionCallback unhandled_hook) noexcept
{HSM_ASSERT(initial_state != nullptr);HSM_ASSERT(entry_path_buffer != nullptr);HSM_ASSERT(buffer_size > 0);bool valid = (initial_state != nullptr) && (entry_path_buffer != nullptr) && (buffer_size > 0);if (!valid) return;_user_data = user_data;_unhandled_hook = unhandled_hook;_initial_state = initial_state;_entry_path_buffer = entry_path_buffer;_buffer_size = buffer_size;_current_state = nullptr;_perform_transition(initial_state, nullptr);
}inline void StateMachine::deinit() noexcept
{_current_state = nullptr;_initial_state = nullptr;_user_data = nullptr;_unhandled_hook = ActionCallback{nullptr, nullptr};_entry_path_buffer = nullptr;_buffer_size = 0;
}inline void StateMachine::reset() noexcept
{if (_initial_state != nullptr) _perform_transition(_initial_state, nullptr);
}inline bool StateMachine::dispatch(const Event* event) noexcept
{HSM_ASSERT(event != nullptr);HSM_ASSERT(_current_state != nullptr);if ((_unhandled_hook.fn != nullptr) && (event != nullptr)){_unhandled_hook.fn(_unhandled_hook.ctx, this, event);}const State* state_iter = _current_state;bool handled = false;while (state_iter != nullptr){if (_process_state_transitions(state_iter, event)){handled = true;break;}state_iter = state_iter->parent;}if ((!handled) && (_unhandled_hook.fn != nullptr)){_unhandled_hook.fn(_unhandled_hook.ctx, this, event);}return handled;
}inline bool StateMachine::is_in_state(const State* state) const noexcept
{HSM_ASSERT(state != nullptr);HSM_ASSERT(_current_state != nullptr);const State* iter = _current_state;while (iter != nullptr){if (iter == state) return true;iter = iter->parent;}return false;
}inline const char* StateMachine::get_current_state_name() const noexcept
{HSM_ASSERT(_current_state != nullptr);if (_current_state->name != nullptr) return _current_state->name;return "Unknown";
}/* Private helpers */inline uint8_t StateMachine::_get_state_depth(const State* state) const noexcept
{HSM_ASSERT(state != nullptr);uint8_t depth = 0;const State* cur = state;while (cur != nullptr){++depth;cur = cur->parent;}return depth;
}inline const State* StateMachine::_find_lca(const State* s1, const State* s2) const noexcept
{if (s1 == nullptr) return s2;if (s2 == nullptr) return s1;const State* p1 = s1;const State* p2 = s2;uint8_t d1 = _get_state_depth(p1);uint8_t d2 = _get_state_depth(p2);while (d1 > d2) { p1 = p1->parent; --d1; }while (d2 > d1) { p2 = p2->parent; --d2; }while (p1 != p2) { p1 = p1->parent; p2 = p2->parent; }return p1;
}inline void StateMachine::_perform_transition(const State* target_state, const Event* event) noexcept
{HSM_ASSERT(target_state != nullptr);const State* source_state = _current_state;bool same_state = (source_state == target_state);if (same_state){if ((source_state != nullptr) && (source_state->exit_action.fn != nullptr))source_state->exit_action.fn(source_state->exit_action.ctx, this, event);if (target_state->entry_action.fn != nullptr)target_state->entry_action.fn(target_state->entry_action.ctx, this, event);}else{const State* lca = _find_lca(source_state, target_state);_execute_exit_actions(source_state, lca, event);int path_length = _build_entry_path(target_state, lca);if (path_length < 0){HSM_ASSERT(0);return;}_current_state = target_state;_execute_entry_actions(static_cast<uint8_t>(path_length), event);}
}inline const Transition* StateMachine::_find_matching_transition(const State* state, const Event* event, bool* guard_passed) const noexcept
{HSM_ASSERT(state != nullptr);HSM_ASSERT(event != nullptr);HSM_ASSERT(state->transitions != nullptr);HSM_ASSERT(state->num_transitions != 0);if (guard_passed) *guard_passed = false;for (size_t i = 0; i < state->num_transitions; ++i){const Transition* t = &state->transitions[i];if (t->event_id == event->id){bool g = true;if (t->guard.fn != nullptr){g = t->guard.fn(t->guard.ctx, this, event);}if (g){if (guard_passed) *guard_passed = true;return t;}}}return nullptr;
}inline bool StateMachine::_execute_transition(const Transition* transition, const Event* event) noexcept
{HSM_ASSERT(transition != nullptr);HSM_ASSERT(event != nullptr);if (transition->type == TransitionType::Internal){if (transition->action.fn != nullptr)transition->action.fn(transition->action.ctx, this, event);}else{if (transition->action.fn != nullptr)transition->action.fn(transition->action.ctx, this, event);_perform_transition(transition->target, event);}return true;
}inline bool StateMachine::_process_state_transitions(const State* state, const Event* event) noexcept
{HSM_ASSERT(state != nullptr);HSM_ASSERT(event != nullptr);bool guard_passed = false;const Transition* matching = _find_matching_transition(state, event, &guard_passed);if ((matching != nullptr) && guard_passed){return _execute_transition(matching, event);}return false;
}inline void StateMachine::_execute_exit_actions(const State* source_state, const State* lca, const Event* event) noexcept
{const State* iter = source_state;while ((iter != nullptr) && (iter != lca)){if (iter->exit_action.fn != nullptr)iter->exit_action.fn(iter->exit_action.ctx, this, event);iter = iter->parent;}
}inline int StateMachine::_build_entry_path(const State* target_state, const State* lca) noexcept
{HSM_ASSERT(target_state != nullptr);HSM_ASSERT(_entry_path_buffer != nullptr);HSM_ASSERT(_buffer_size > 0);const State* iter = target_state;uint8_t idx = 0;while ((iter != nullptr) && (iter != lca)){if (idx < _buffer_size){_entry_path_buffer[idx] = iter;++idx;iter = iter->parent;}else{return -1; // buffer insufficient}}return static_cast<int>(idx);
}inline void StateMachine::_execute_entry_actions(uint8_t path_length, const Event* event) noexcept
{HSM_ASSERT(_entry_path_buffer != nullptr);int idx = static_cast<int>(path_length) - 1;for (; idx >= 0; --idx){const State* s = _entry_path_buffer[idx];if ((s != nullptr) && (s->entry_action.fn != nullptr))s->entry_action.fn(s->entry_action.ctx, this, event);}
}/* ---------------- Definitions that require complete StateMachine type ---------------- */
namespace detail {template<typename T, void (T::*MF)(StateMachine*, const Event*)>
inline void action_from_sm_user_data(void* /*ctx*/, StateMachine* sm, const Event* ev) {T* obj = static_cast<T*>(sm->user_data());(obj->*MF)(sm, ev);
}template<typename T, bool (T::*MG)(const StateMachine*, const Event*)>
inline bool guard_from_sm_user_data(void* /*ctx*/, const StateMachine* sm, const Event* ev) {T* obj = static_cast<T*>(sm->user_data());return (obj->*MG)(sm, ev);
}} // namespace detail} // namespace hsm#endif // STATE_MACHINE_HPP
// example.cpp
// Updated to match state_machine.hpp changes:
// - Guard member functions now accept `const StateMachine*`
// - use make_guard_using_sm_user_data accordingly#include "state_machine.hpp"
#include <iostream>// Simple IDs
enum : uint32_t { EVT_START = 1, EVT_STOP = 2, EVT_TICK = 3 };struct Controller
{// Actions still take non-const StateMachine*void on_entry(hsm::StateMachine* /*sm*/, const hsm::Event* /*e*/) {std::cout << "Controller::on_entry\n";}void on_exit(hsm::StateMachine* /*sm*/, const hsm::Event* /*e*/) {std::cout << "Controller::on_exit\n";}// Guard now receives a const StateMachine*bool guard_allow(const hsm::StateMachine* /*sm*/, const hsm::Event* /*e*/) {std::cout << "Controller::guard_allow -> true\n";return true;}void handle_start(hsm::StateMachine* /*sm*/, const hsm::Event* /*e*/) {std::cout << "Controller::handle_start\n";}void handle_stop(hsm::StateMachine* /*sm*/, const hsm::Event* /*e*/) {std::cout << "Controller::handle_stop\n";}
};// Forward declare states
extern const hsm::State S_idle;
extern const hsm::State S_running;// Transitions for idle
static const hsm::Transition T_idle[] = {// EVT_START -> S_running, guard using sm->user_data, action using sm->user_data{ EVT_START, &S_running,hsm::make_guard_using_sm_user_data<Controller, &Controller::guard_allow>(),hsm::make_action_using_sm_user_data<Controller, &Controller::handle_start>(),hsm::TransitionType::External}
};// Transitions for running
static const hsm::Transition T_running[] = {{ EVT_STOP, &S_idle,hsm::make_guard_using_sm_user_data<Controller, &Controller::guard_allow>(),hsm::make_action_using_sm_user_data<Controller, &Controller::handle_stop>(),hsm::TransitionType::External},{ EVT_TICK, &S_running,hsm::GuardCallback{nullptr, nullptr}, // no guardhsm::ActionCallback{nullptr, nullptr}, // no actionhsm::TransitionType::Internal}
};// State definitions
const hsm::State S_idle = {nullptr,hsm::make_action_using_sm_user_data<Controller, &Controller::on_entry>(),hsm::make_action_using_sm_user_data<Controller, &Controller::on_exit>(),T_idle, sizeof(T_idle)/sizeof(T_idle[0]),"Idle"
};const hsm::State S_running = {nullptr,hsm::make_action_using_sm_user_data<Controller, &Controller::on_entry>(),hsm::make_action_using_sm_user_data<Controller, &Controller::on_exit>(),T_running, sizeof(T_running)/sizeof(T_running[0]),"Running"
};int main()
{Controller ctrl;// entry path buffer sized for max hierarchy depth (2 here)const hsm::State* buffer[4];hsm::StateMachine sm;sm.init(&S_idle, buffer, 4, &ctrl, hsm::ActionCallback{nullptr, nullptr});std::cout << "Current state: " << sm.get_current_state_name() << "\n";hsm::Event ev_start{EVT_START, nullptr};sm.dispatch(&ev_start); // should transition to Running and call handle_startstd::cout << "After START state: " << sm.get_current_state_name() << "\n";hsm::Event ev_stop{EVT_STOP, nullptr};sm.dispatch(&ev_stop); // back to Idlestd::cout << "After STOP state: " << sm.get_current_state_name() << "\n";return 0;
}
/*
$ ./example_bin
Current state: Idle
Controller::guard_allow -> true
Controller::handle_start
Controller::on_exit
Controller::on_entry
After START state: Running
Controller::guard_allow -> true
Controller::handle_stop
Controller::on_exit
Controller::on_entry
After STOP state: Idle
*/