我有一个FSM,其中每个状态都表示为一个类。所有状态都来自一个通用基类,并且具有一个用于处理输入的虚函数。
由于一次只能激活一个状态,因此所有可能的状态都存储在FSM类内部的联合中。
由于所有状态(包括基类)都按值存储,因此我不能直接使用虚拟路径。相反,我使用static_cast创建对联合中基础对象的引用,然后通过该引用调用虚拟方法。这适用于GCC。在Clang上不起作用。
这是一个最小的示例:
#include <iostream>
#include <string>
struct State {
virtual std::string do_the_thing();
virtual ~State() {}
};
struct IdleState: State {
std::string do_the_thing() override;
};
std::string State::do_the_thing() {
return "State::do_the_thing() is called";
}
std::string IdleState::do_the_thing() {
return "IdleState::do_the_thing() is called";
}
int main() {
union U {
U() : idle_state() {}
~U() { idle_state.~IdleState(); }
State state;
IdleState idle_state;
} mem;
std::cout
<< "By reference: "
<< static_cast<State&>(mem.state).do_the_thing()
<< "\n";
std::cout
<< "By pointer: "
<< static_cast<State*>(&mem.state)->do_the_thing()
<< "\n";
}
当我使用GCC 8.2.1编译此代码时,程序的输出为:
By reference: IdleState::do_the_thing() is called
By pointer: State::do_the_thing() is called
当我用Clang 8.0.0编译时,输出为:
By reference: State::do_the_thing() is called
By pointer: IdleState::do_the_thing() is called
因此,这两个编译器的行为是相反的:GCC仅通过引用执行虚拟调度,仅通过指针执行Clang。
我发现的一种解决方案是使用reinterpret_cast<State&>(mem)
(这样就可以将联合本身转换为State&
)。这对两个编译器都有效,但是我仍然不确定它的可移植性。我将基类加入联合的原因是,首先要特别避免使用reinterpret_cast ...
那么在这种情况下强制执行虚拟调度的正确方法是什么?
更新
总而言之,一种实现方法是在联合(或std :: variant)外部使用一个单独的基类类型的指针,该指针指向当前的活动成员。
像访问基类一样直接访问联合中的子类是不安全的。
答案 0 :(得分:3)
您访问联盟的非活动成员。该程序的行为是不确定的。
工会的所有成员都是国家的子类,这意味着无论工会的哪个成员活跃,我仍然可以使用字段
state
这并不意味着。
一种解决方案是分别存储指向基础对象的指针。此外,您需要跟踪当前处于活动状态的联合状态。这是使用变体类最简单的解决方法:
class U {
public:
U() {
set<IdleState>();
}
// copy and move functions left as an exercise
U(const U&) = delete;
U& operator=(const U&) = delete;
State& get() { return *active_state; }
template<class T>
void set() {
storage = T{};
active_state = &std::get<T>(storage);
}
private:
State* active_state;
std::variant<IdleState, State> storage;
};
// usage
U mem;
std::cout << mem.get().do_the_thing();
答案 1 :(得分:0)
使用
std::cout
<< "By reference: "
<< static_cast<State&>(mem.state).do_the_thing()
<< "\n";
是错误的。由于mem.state
尚未初始化,并且不是mem
的活动成员,因此会导致未定义的行为。
我建议改变策略。
union
。class
/ struct
。它可以指向State
的任何子类型。State
为抽象基类以禁止其实例化。
class State {
public:
virtual std::string do_the_thing() = 0;
protected:
State() {}
virtual ~State() = 0 {}
};
// ...
// More code from your post
// ...
struct StateHolder
{
std::unique_ptr<State> theState; // Can be a shared_ptr too.
};
int main()
{
StateHolder sh;
sh.theState = new IdleState;
std::cout << sh.theState->do_the_thing() << std::endl;
}
答案 2 :(得分:0)
因此,eerorika的回答启发了我以下解决方案。它与我最初的状态有点接近(没有单独的指向工会成员的指针),但是我将所有肮脏的工作委托给了std :: variant(而不是工会)。
#include <iostream>
#include <variant>
#include <utility>
// variant_cb is a std::variant with a
// common base class for all variants.
template<typename Interface, typename... Variants>
class variant_cb {
static_assert(
(sizeof...(Variants) > 0),
"At least one variant expected, got zero.");
static_assert(
(std::is_base_of<Interface, Variants>::value && ...),
"All members of variant_cb must have the same base class "
"(the first template parameter).");
public:
variant_cb() = default;
template<typename T>
variant_cb(T v) : v(v) {}
variant_cb(const variant_cb&) = default;
variant_cb(variant_cb&&) = default;
variant_cb& operator=(const variant_cb&) = default;
variant_cb& operator=(variant_cb&&) = default;
Interface& get() {
return std::visit([](Interface& x) -> Interface& {
return x;
}, v);
}
template <typename T>
Interface& set() {
v = T{};
return std::get<T>(v);
}
private:
std::variant<Variants...> v;
};
// Usage:
class FSM {
public:
enum Input { DO_THE_THING, /* ... */ };
void handle_input(Input input) {
auto& state = current_state.get();
current_state = state(input);
}
private:
struct State;
struct Idle;
struct Active;
using AnyState = variant_cb<State, Idle, Active>;
template<typename T>
static AnyState next_state() {
return {std::in_place_type<T>};
}
struct State {
virtual ~State() {};
virtual AnyState operator()(Input) = 0;
};
struct Idle: State {
AnyState operator()(Input) override {
std::cout << "Idle -> Active\n";
return next_state<Active>();
}
};
struct Active: State {
int countdown = 3;
AnyState operator()(Input) override {
if (countdown > 0) {
std::cout << countdown << "\n";
countdown--;
return *this;
} else {
std::cout << "Active -> Idle\n";
return next_state<Idle>();
}
}
};
AnyState current_state;
};
int main() {
FSM fsm;
for (int i = 0; i < 5; i++) {
fsm.handle_input(FSM::DO_THE_THING);
}
// Output:
//
// Idle -> Active
// 3
// 2
// 1
// Active -> Idle
}