C++ 中的 Lambda 泛型

Posted

技术标签:

【中文标题】C++ 中的 Lambda 泛型【英文标题】:Lambda genericity in C++ 【发布时间】:2019-10-05 12:20:28 【问题描述】:

在 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 是一个 lambda,它接受 Event,对它做一些事情,但什么也不返回。我有一张与事件类型相关联的EventHandler 地图。 (地图在这个 MWE 中指向 EventHandler,但在我最初的实现中,它当然指向一个向量)

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 的处理程序,但我想在采用接口的lambda 和采用该接口的实现的lambda 之间没有继承链接,所以it fails 因为它不能分配std::function&lt;void(SomeEvent)&gt;到 ``std::function` 类型的变量。

我一直在为此摸不着头脑,我的 C++ 生锈了,根本不是最新的。你会怎么做?语言中是否存在允许我以通用方式指定 lambda 的模式或功能?尝试在EventHandler 定义中使用auto,但似乎不允许。

【问题讨论】:

如果您想使用函数指针来访问它,您似乎无法在 lambda 中捕获任何内容:请参阅***.com/questions/28746744/… @PabloOliva 真的是我的问题吗?我没有使用函数指针。我的理解是 std::function 不是函数指针,您可以使用捕获 lambda。 【参考方案1】:

嗯,C++ 的趋势是更喜欢静态多态性而不是动态多态性(实际上,静态或编译时的一切真的......);在您的情况下 - 您不能在运行时出现意外事件 types

所以,也许您可​​以忘记示例中的大部分代码并尝试基于 std::variant 的内容:

在事件类型上模板化您的事件处理代码。 而不是EventType - 只需在事件上使用std::variant::index() 即可在可能的事件类型中获取其数字类型索引;或者更好 - 根本不要使用它,您的事件类型模板可能就足够了。 您将拥有using Event = std::variant&lt;events::Keyboard, events::Foo, events::Some&gt;,而不是class Eventclass SomeEvent : Event。也许你会有
namespace events  
    class Base  void bar()  /* ... */  ;
    class Foo : class Base  /* ... */ ;

所有事件都继承自 Base(但非虚拟)。 Event 不需要显式存储或读取其类型 - 它总是会知道(除非您主动进行类型擦除)。 不再需要传递Event*,您可以通过值或移动引用传递Events。所以不再有 std::unique_ptrs 的类不匹配。

至于您的 lambda,它很可能只是:

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

event_handler 是 visitor。

现在 - 确实不能总是逃避动态多态性;有时你可以,但你所在的组织/公司太迟缓/笨拙,不允许这种情况发生。但是,如果你能做到这一点,它可能会为你省去很多麻烦。

【讨论】:

有趣的答案。它实现了我一直在寻找的东西。然而,Event 作为“变体”列表的声明似乎是有限的。我还不知道std::variant,但似乎你不能用这种方法事后声明额外的事件类型,对吧? @JeffGarett 的答案对我来说看起来更简单。我想知道你对此有何想法。 @LukeSkywalker:确实,有一个变体——你使用的是封闭列表。但只要您只通过源代码而不是在运行时引入新事件,就可以了。【参考方案2】:

正如在另一个答案中提到的,这是一个拥有大量现有艺术的领域。更多研究的推荐搜索词是“double dispatch”、“multiple dispatch”和“open multi-methods”。

在处理此类问题时,我发现最好考虑一个人在哪里拥有信息以及如何将其传输到需要的地方。添加处理程序时,您具有事件类型,并且在使用它时需要将事件重构为正确的类型。因此,另一个答案关于在处理程序周围添加额外逻辑的建议是一种不错的方法。但是,您不希望您的用户这样做。如果您将事件类型的知识向下发送一层,您可以为您的用户自己包装,例如:

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();
);

这和以前一样容易或困难,但不是使用枚举器,而是传递模板参数以传达事件类型。

一个非常相关的问题是摆脱容易出错的样板get_event_type。如果有人指定了错误的值,就会发生不好的事情。如果你可以使用RTTI,你可以让编译器决定一个合理的map key:

// 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 类型和实际动态类型之间的不匹配也可能是一个错误,因此它是双向的。

【讨论】:

完美。我认为可以接受的形式的工作示例:ideone.com/8EUmMW。谢谢@Jeff Garett 从我的实现中我可能有的最后一个顾虑是需要为add_event_handler 中的演员转移unique_ptr。 您的担忧之一是清晰吗?还是……? 我不喜欢从 Event 转换为 EventType 并因此在每个事件插入时重新创建一个 unique_ptr 的需要。在@einpoklum 的回答之后,我删除了指针并将它们替换为r 值引用。这是更新后的代码:ideone.com/qGZvXr 很公平。我也更喜欢这样。小注意,我后来意识到,在 insert_event 中,使用 operator[] 而不检查类型是否在映射中可能会导致 bad_function_call 异常,如果不是(因为它默认构造一个函数然后调用它)。【参考方案3】:

我认为您的问题来自std::unique_ptr&lt;Event&gt;std::unique_ptr&lt;SomeEvent&gt; 不兼容的事实。它被转移到std::function&lt;...&gt; 部分,因为您有一个接受EventSomeEvent 的函数。从技术上讲,您可以使用 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());

【讨论】:

谢谢,我虽然想过,但我想避免处理程序不得不做一些无关紧要的事情。对我来说,让事件循环的用户保持简单是很重要的,即使它需要在事件循环方面做更多的工作。我虽然关于访问者,但似乎它会限制我扩展到用户定义的事件的能力。

以上是关于C++ 中的 Lambda 泛型的主要内容,如果未能解决你的问题,请参考以下文章

Kotlin 泛型中的 in 和 out

Java泛型中的通配符

java 泛型中的T和?

Java泛型中的协变和逆变

java泛型中的下限

Java泛型中的标记符含义