文章目錄
- 概述
- 為什么要遷移到 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
*/