如何可靠地对对象的方法强制执行虚拟调度?

时间:2019-05-02 21:25:51

标签: c++ state-machine fsm

上下文

我有一个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)外部使用一个单独的基类类型的指针,该指针指向当前的活动成员。

像访问基类一样直接访问联合中的子类是不安全的。

3 个答案:

答案 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的活动成员,因此会导致未定义的行为。


我建议改变策略。

  1. 请勿使用union
  2. 使用包含智能指针的常规class / struct。它可以指向State的任何子类型。
  3. 使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
}