Лямбда универсальность в C ++ - PullRequest
0 голосов
/ 05 октября 2019

Реализуя простой игрушечный цикл событий в C ++, я столкнулся с проблемой. У меня есть интерфейс (абстрактный класс) Event, который реализуется различными типами событий (клавиатура, асинхронные вызовы, ...). В моем примере SomeEvent - это реализация Event.

enum class EventType {
  SOME_EVENT = 0,
};

class Event {
public:
  virtual ~Event() {}
  virtual EventType get_event_type() = 0;
};

class SomeEvent: public Event {
public:
  SomeEvent(): m_event_type(EventType::SOME_EVENT) {}
  void bar() { std::cout << "hello" << std::endl; }
  EventType get_event_type() { return this->m_event_type; }

public:
  EventType m_event_type;
};

EventHandler - это лямбда, которая принимает Event, что-то с ней делает и ничего не возвращает. У меня есть карта EventHandler, связанная с типом события. (Карта указывает на EventHandler в этом MWE, но в моей первоначальной реализации она, конечно, указывает на вектор)

typedef std::function<void(std::unique_ptr<Event>)> EventHandler;

std::map<EventType, EventHandler> event_handlers;

template <class T>
void add_event_handler(
  EventType event_type,
  const std::function<void(std::unique_ptr<T>)> &event_handler) {
  event_handlers[event_type] = event_handler; // Does not compile
}

Когда я вставляю событие с помощью inject_event, я просто получаю обработчик событияи назовите его с событием в качестве аргумента.

void inject_event(std::unique_ptr<Event> event) {
  event_handlers[event->get_event_type()](std::move(event));
}

Проблема заключается в универсальности этой карты. Он должен содержать обработчик, который принимает Event, но я полагаю, что нет никакого наследования между лямбда-выражением, берущим интерфейс, и лямбда-выражением, берущим реализацию этого интерфейса, поэтому он терпит неудачу , потому что не можетприсвойте std::function<void(SomeEvent)> переменной типа `` std :: function`.

Я ломал голову над этим, и мой C ++ ржавый И совсем не обновляется. Как бы вы пошли об этом? Есть ли в языке шаблон или функциональность, которые позволили бы мне определять лямбду в общем виде? Пробовал использовать auto в EventHandler определении, но это не разрешено.

Ответы [ 3 ]

2 голосов
/ 07 октября 2019

Что ж, в C ++ существует тенденция отдавать предпочтение статическому, а не динамическому полиморфизму (и на самом деле статическому или компиляции во всем на самом деле ...);и в вашем случае - вы не можете иметь непредвиденное событие типов во время выполнения.

Так что, возможно, вы можете забыть о большей части кода в вашем примере и попробовать что-то на основе std::variant:

  • Шаблон вашего кода обработки события для типа события.
  • Вместо EventType - просто используйте std::variant::index() для события, чтобыполучить его числовой индекс типа среди возможных типов событий;или еще лучше - не используйте его вообще, вашего шаблона типа события может быть достаточно.
  • Вместо class Event и class SomeEvent : Event у вас будет using Event = std::variant<events::Keyboard, events::Foo, events::Some>. Возможно, у вас будет
    namespace events { 
        class Base { void bar() { /* ... */ } };
        class Foo : class Base { /* ... */ };
    }
    
    со всеми событиями, наследуемыми от Base (но не виртуально).
  • и Event не нужно будет явно сохранять или читать его тип - он всегда будет просто знать(если вы не активно набираете это).
  • Больше не нужно передавать Event*, вы можете просто передать Event s по значению или по ссылкам перемещения. Так что не больше std::unique_ptr s с несовпадающими классами.
  • Что касается вашей лямбды, она вполне может стать просто:

    void inject_event(Event&& event) {
        std::visit(event_handlers,event);
    }
    

    с event_handler посетителем .

Теперь - это правда, что вы не всегда можете избежать динамического полиморфизма;и иногда вы можете, но организация / компания, в которой вы работаете, слишком медлительна / неуклюжа, чтобы это произошло. Однако, если вы справитесь, это может избавить вас от многих головных болей.

1 голос
/ 07 октября 2019

Как упоминалось в другом ответе, это область с большим количеством ранее существовавшего искусства. Рекомендованные поисковые термины для дополнительных исследований: «двойная отправка», «множественная отправка» и «открытые мульти-методы».

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

template<typename EventParameter, typename Function>
void add_event_handler(Function&& f)
{
    event_handlers[event_type<EventParameter>()] = [=](std::unique_ptr<Event> e) {
        f(event_cast<EventParameter>(std::move(e)));
    };
}

Пользователи могут использовать это легко, с производным типом, без дополнительной упаковки требуетсяв конце:

add_event_handler<SomeEvent>([](std::unique_ptr<SomeEvent>) {
    e->bar();
});

Это примерно так же просто или сложно, как и раньше, но вместо перечислителя передается аргумент шаблона, чтобы сообщить тип события.

Aочень связанное беспокойство избавляется от склонного к ошибкам шаблона get_event_type. Если кто-то указывает неправильное значение, случаются плохие вещи. Если вы можете использовать RTTI, вы можете позволить компилятору выбрать разумный ключ карты:

// Key for the static type without an event
template<typename EventParameter>
auto event_type() -> std::type_index
{
    return std::type_index(typeid(EventParameter));
}

// Key for the dynamic type given an event    
template<typename EventParameter>
auto event_type(const EventParameter& event) -> std::type_index
{
    return std::type_index(typeid(event));
}

std::map<std::type_index, EventHandler> event_handlers;

Это связано, поскольку вы можете наложить ограничение на то, что ключ карты и аргумент обработчика совпадают, тем дешевле вы можете преобразовать событие обратно в исходную форму. Например, в приведенном выше примере static_cast может работать:

template<typename To, typename From>
std::unique_ptr<To> event_cast(std::unique_ptr<From> ptr)
{
    return static_cast<To*>(ptr.release());
}

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

0 голосов
/ 05 октября 2019

Я думаю, что ваша проблема связана с тем, что std::unique_ptr<Event> не совместим с std::unique_ptr<SomeEvent>. Он переносится на часть std::function<...>, поскольку у вас есть функция, которая принимает Event или SomeEvent. Технически вы могли бы вызвать бывшего с SomeEvent, но не наоборот. Однако вы не можете вставить то, что обрабатывает SomeEvent, в список, который обрабатывает обычные Event.

. То, что вы пытаетесь достичь, - это двойная диспетчеризация, основанная на типе аргумента. С вашей текущей настройкой лучшее, что вы можете сделать, - это иметь общие обработчики Event в коллекции и передавать экземпляры SomeEvent. Если вы хотите вызывать разные обработчики для SomeEvent экземпляров, вам нужен двойной шаблон отправки / посетителя. Вы можете (внутри обработчика) попробовать и динамически привести ваш указатель к SomeEvent, если вы абсолютно уверены, что это правильный тип. Это все еще лучше всего работает с голыми указателями. Например:

auto handler = [](Event* event){
  if (SomeEvent* someEvent = std::dynamic_cast<SomeEvent*>(event); someEvent != null){  
    // handle some event
  }
}

add_event_handler(EventType::SOME_EVENT, handler);
typedef std::function<void(Event*)> EventHandler;

std::map<EventType, EventHandler> event_handlers;

void add_event_handler(
  EventType event_type,
  const std::function<void(Event*)> &event_handler) {
  event_handlers[event_type] = event_handler;
}
void inject_event(std::unique_ptr<Event> event) {
  event_handlers[event->get_event_type()](event.get());
}
...