Как надежно форсировать виртуальную диспетчеризацию по методу объекта? - PullRequest
2 голосов
/ 03 мая 2019

Контекст

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

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

Ответы [ 3 ]

3 голосов
/ 03 мая 2019

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

все члены союза являются подклассами государства, что означает, что независимо от того, какой член союза является активным, я все еще могу использовать поле 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();
0 голосов
/ 04 мая 2019

Итак, ответ 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 голосов
/ 03 мая 2019

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

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;
 }
...