如何可靠地强制对对象的方法进行虚拟分派?

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 的活动成员。


我建议改变策略。

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