如何可靠地强制对对象的方法进行虚拟分派?
How to reliably force virtual dispatch on an object's method?
上下文
我有一个 FSM,其中每个状态都表示为 class。所有状态都源自一个共同的基础 class 并具有一个用于处理输入的虚函数。
由于一次只能激活一个状态,因此所有可能的状态都存储在 FSM class.
内的联合中
问题
由于所有状态(包括基础class)都是按值存储的,我不能直接使用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&
)。这适用于两个编译器,但我仍然不确定它的可移植性。我将基数 class 放在联合中的原因是首先要特别避免 reinterpret_cast...
那么在这种情况下强制虚拟调度的正确方法是什么?
更新
总而言之,一种方法是在联合(或 std::variant)之外有一个单独的基 class' 类型的指针,它指向当前的活动成员。
直接访问联合中的子class 就好像它是基class 是不安全的。
使用
std::cout
<< "By reference: "
<< static_cast<State&>(mem.state).do_the_thing()
<< "\n";
错了。它会导致未定义的行为,因为 mem.state
尚未初始化并且不是 mem
的活动成员。
我建议改变策略。
- 不要使用
union
。
- 使用包含智能指针的常规
class
/struct
。它可以指向 State
. 的任何子类型的实例
- 将
State
设为抽象基础 class 以禁止其实例化。
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;
}
您访问了工会的非活跃成员。程序的行为未定义。
all members of the union are subclasses of State, which means no matter what member of the union is active, I can still use the field state
不是那个意思
一个解决方案是单独存储一个指向基础对象的指针。此外,您需要跟踪当前处于活动状态的联合状态。这是使用变体 class:
最简单的解决方法
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();
因此,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
}
上下文
我有一个 FSM,其中每个状态都表示为 class。所有状态都源自一个共同的基础 class 并具有一个用于处理输入的虚函数。
由于一次只能激活一个状态,因此所有可能的状态都存储在 FSM class.
内的联合中问题
由于所有状态(包括基础class)都是按值存储的,我不能直接使用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&
)。这适用于两个编译器,但我仍然不确定它的可移植性。我将基数 class 放在联合中的原因是首先要特别避免 reinterpret_cast...
那么在这种情况下强制虚拟调度的正确方法是什么?
更新
总而言之,一种方法是在联合(或 std::variant)之外有一个单独的基 class' 类型的指针,它指向当前的活动成员。
直接访问联合中的子class 就好像它是基class 是不安全的。
使用
std::cout
<< "By reference: "
<< static_cast<State&>(mem.state).do_the_thing()
<< "\n";
错了。它会导致未定义的行为,因为 mem.state
尚未初始化并且不是 mem
的活动成员。
我建议改变策略。
- 不要使用
union
。 - 使用包含智能指针的常规
class
/struct
。它可以指向State
. 的任何子类型的实例
- 将
State
设为抽象基础 class 以禁止其实例化。
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;
}
您访问了工会的非活跃成员。程序的行为未定义。
all members of the union are subclasses of State, which means no matter what member of the union is active, I can still use the field
state
不是那个意思
一个解决方案是单独存储一个指向基础对象的指针。此外,您需要跟踪当前处于活动状态的联合状态。这是使用变体 class:
最简单的解决方法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();
因此,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
}