Контекст

У меня есть 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 :: variable), который указывает на текущий активный член.

Прямой доступ к подклассу в объединении, как если бы это был базовый класс, небезопасен.

2
ea7ababe 3 Май 2019 в 00:25

3 ответа

Лучший ответ

Вы получаете доступ к неактивному члену профсоюза. Поведение программы не определено.

все члены объединения являются подклассами State, что означает, что независимо от того, какой член объединения является активным, я все еще могу использовать поле 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();
3
eerorika 2 Май 2019 в 22:29

Использование

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;
 }
0
R Sahu 2 Май 2019 в 21:45

Таким образом, ответ eerorika вдохновил меня на следующее решение. Это немного ближе к тому, что у меня было изначально (нет отдельного указателя на члены объединения), но я делегировал всю грязную работу std :: варианту (вместо объединения).

#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
}
0
ea7ababe 4 Май 2019 в 01:17