Шаблонная статическая карта шаблонных объектов с различными списками параметров - PullRequest
0 голосов
/ 07 ноября 2019

Я спрашиваю о создании шаблонной карты объектов разных типов, пример Observer - просто удобный способ описать проблему.

Итак, у меня есть простая реализация шаблона Observer:

template<typename ...A> class Observable
{
    typedef void(*EventListener)(A...);  /* Listener callback type */
public:
    void addListener(EventListener listener);
    void removeListener(EventListener listener);
    void fireEvent(A... args);
};

В моем коде есть разные наборы событий с разными сигнатурами слушателя, каждый из которых представлен enum, то есть:

enum PCEvents {
  JUMPED = 0,       // void (*fn)(int, int)
  WALKED = 1,       // void (*fn)(int)
  DIED = 2,         // void (*fn)()
  DIED_HORRIBLY = 3 // void (*fn)(const std::string&)
};

Теперь я хочу зонтичный класс, который инкапсулирует все Observables, основанные на перечислении. Я не знаю, как это можно реализовать, поэтому вот его желаемая реализация:

void gameOver(const std::string& deathCause)
{
    std::cout << "You perished because of " << deathCause << std::endl;
}

int main()
{
    // parameter list syntax is lax 
    PoorGuyObserver<PCEvents, void (*)(int, int), void (*)(int), void (*)(), void (*)(const std::string&)> observer;

    observer.addListener(PCEvents::DIED_HORRIBLY, gameOver);
    observer.fireEvent(PCEvents::DIED_HORRIBLY, "gazeebo");
}

Меня интересует решение для c ++ 14.

1 Ответ

1 голос
/ 07 ноября 2019

Моя интерпретация вашей проблемы состояла в том, что вы хотели описать наблюдаемое как карту значений перечисления для сигнатур функций и использовать вариационный шаблон для его генерации.

Один хитрый момент с вариационными шаблонами заключается в том, что выне может смешивать постоянные значения с именами типов. Конечный бит должен быть либо всеми именами типов (typename...), либо всеми константами (auto...). Поэтому первым шагом для меня было создать вспомогательный тип:

template <typename TEnum, TEnum Id, typename... TArgs>
struct Event;

Примечание. Если вы используете c ++ 17, вы можете объединить первые два аргумента как auto Id, но выуказанный C ++ 14

Следующий шаг: реализовать наблюдаемое для одного Event. Обычно это будет часть рекурсивного вариационного шаблона, но это было необходимо для сложной задачи, которую я опишу позже.

template <typename TEnum, TEnum Id, typename... TArgs>
struct ObservableImpl {
    std::vector<std::function<void(TArgs...)>> subscribers;

    template <TEnum FireId, typename = std::enable_if_t<FireId == Id>>
    void fire(TArgs... args) { 
        for (auto subscriber : subscribers) { 
            subscriber(args...); 
        }
    }

    template <TEnum SubscribeId, typename = std::enable_if_t<SubscribeId == Id>>
    void subscribe(std::function<void (TArgs...)> handler) {
        subscribers.push_back(handler);
    }
};

Затем рекурсивная часть. Каждый уровень наследования наследуется от реализации отдельного события и поддерживает fire и subscribe.

template <typename... TEvents>
struct Observable;

// Recursive case, implement the current event, inherit from the next
template <typename TEnum, TEnum Id, typename... TArgs, typename... TEvents>
struct Observable<Event<TEnum, Id, TArgs...>, TEvents...> : public ObservableImpl<TEnum, Id, TArgs...>, Observable<TEvents...> {
    using Observable<TEvents...>::subscribe;
    using Observable<TEvents...>::fire;
    using ObservableImpl<TEnum, Id, TArgs...>::subscribe;
    using ObservableImpl<TEnum, Id, TArgs...>::fire;
};

// Terminal case, implement the last event
template <typename TEnum, TEnum Id, typename... TArgs>
struct Observable<Event<TEnum, Id, TArgs...>> : public ObservableImpl<TEnum, Id, TArgs...> {
    using ObservableImpl<TEnum, Id, TArgs...>::subscribe;
    using ObservableImpl<TEnum, Id, TArgs...>::fire;
};

Во многих примерах завершающая специализация пуста. Но в нашем случае, поскольку рекурсивная специализация поддерживает базовый метод, в завершающем случае должен быть этот базовый метод. Вот почему я вытащил реализацию с одним событием во второй класс, чтобы сохранить некоторую типизацию.

После этого он готов к использованию:

enum class Events { 
  JUMPED = 0,       // void (*fn)(int, int)
  WALKED = 1,       // void (*fn)(int)
  DIED = 2,         // void (*fn)()
  DIED_HORRIBLY = 3 // void (*fn)(const std::string&)
};

int main(int argc, char** argv) {
    Observable<
        Event<Events, Events::JUMPED, int, int>,
        Event<Events, Events::WALKED, int>,
        Event<Events, Events::DIED>,
        Event<Events, Events::DIED_HORRIBLY, std::string>> observable;

    observable.subscribe<Events::JUMPED>([](int x, int y) { std::cout << "jumped(" << x << ", " << y << ")\n"; });
    observable.subscribe<Events::WALKED>([](int distance) { std::cout << "walked(" << distance << ")\n"; });
    observable.subscribe<Events::DIED>([]() { std::cout << "died()\n"; });
    observable.subscribe<Events::DIED_HORRIBLY>([](std::string how) { std::cout << "died_horribly(" << how << ")\n"; });

    observable.fire<Events::JUMPED>(1, 2);
    observable.fire<Events::WALKED>(42);
    observable.fire<Events::DIED>();
    observable.fire<Events::DIED_HORRIBLY>("fire");
}

https://godbolt.org/z/VhvGPg

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...