【发布时间】:2019-05-02 21:25:50
【问题描述】:
上下文
我有一个 FSM,其中每个状态都表示为一个类。所有状态都派生自一个公共基类,并具有一个用于处理输入的虚函数。
由于一次只能激活一个状态,所有可能的状态都存储在 FSM 类内的联合中。
问题
由于所有状态(包括基类)都是按值存储的,所以我不能直接使用 virtual dispath。相反,我使用 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)之外有一个单独的基类类型指针,它指向当前的活动成员。
像基类一样直接访问联合中的子类是不安全的。
【问题讨论】:
-
"我在联合中创建对基础对象的引用" - 但您没有这样做,而是存储
State对象的实例和实例IdleState对象,然后将非活动字段state转换为导致未定义行为的活动字段类型。有什么东西阻止你直接使用 virtual dispath 吗?请注意,如果您在某处实际存储了引用,它将起作用 -
@VTT,好吧,工会的所有成员都是
State的子类,这意味着无论工会的哪个成员处于活动状态,我仍然可以使用字段state。它是static_cast能够在工会中的任何事情。您首先存储参考的建议实际上似乎有效!虽然我完全不明白为什么......我现在正在尝试。 -
“工会的所有成员都是State的子类,这意味着无论工会的哪个成员处于活动状态,我仍然可以使用字段状态” - 可惜不是,只能访问标准布局类型字段的公共初始序列。具有虚函数的类型不是标准布局类型,因此您会得到未定义的行为。
-
@VTT 而且基础对象不是通用的初始序列。
-
啊,所以我猜这是巧合。在这个简单的例子中,它们在 GCC 中具有相同的二进制布局。
标签: c++ state-machine fsm