C++ lambda 作为带有变量参数的参数

C++ lambda as parameter with variable arguments

提问人:F4LS3 提问时间:11/11/2022 最后编辑:F4LS3 更新时间:11/12/2022 访问量:609

问:

我想创建一个事件系统,该系统使用 lambda 函数作为其订阅者/侦听器,并使用事件类型将它们分配给他们应该订阅的特定事件。lambda 应该有可变参数,因为不同类型的事件使用不同类型的参数/为订阅者提供不同类型的数据。

对于我的调度员,我有以下几点:

class EventDispatcher {
public:
    static void subscribe(EventType event_type, std::function<void(...)> callback);
    void queue_event(Event event);
    void dispatch_queue();

private:
    std::queue<Event*> event_queue;
    std::map<EventType, std::function<void(...)>> event_subscribers;
};

这里没有问题,但是当我去实现我的文件中的函数时,就像这样:subscribe().cpp

void EventDispatcher::subscribe(EventType event_type, std::function<void(...)> callback) {
    ... (nothing here yet)
}

IDE 向我展示了以下内容:

未定义模板 'std::function<void (...) 的隐式实例化>'

C++ lambda 事件处理

评论


答:

0赞 Jeff Garrett 11/12/2022 #1

std::function没有可变参数函数类型的专用化。

你可能想要 .std::function<void()>

评论

0赞 F4LS3 11/12/2022
这是否允许我在 lambda 中使用不同的参数?
0赞 Jeff Garrett 11/13/2022
不。(如果参数不是平凡的,则可变参数函数也不会可移植。通常,对于特定事件类型,您会期望一组固定的类型,因此我会使用这些类型作为参数类型。从逻辑上讲,这与在事件类型中包含事件类型中的相同。
2赞 HolyBlackCat 11/12/2022 #2

不要尝试将具有不同参数类型的事件回调直接放入单个映射中。

相反,创建一个模板来存储回调(按参数类型模板化),并存储指向其非模板库的指针。

我是这样做的:

#include <functional>
#include <iostream>
#include <map>
#include <memory>
#include <queue>
#include <tuple>
#include <typeindex>
#include <typeinfo>
#include <type_traits>
#include <utility>

struct Event
{
    virtual ~Event() = default;
};

struct Observer
{
    virtual ~Observer() = default;
    virtual void Observe(const Event &e) const = 0;
};

template <typename ...P>
struct BasicEvent : Event
{
    std::tuple<P...> params;

    BasicEvent(P ...params) : params(std::move(params)...) {}

    struct EventObserver : Observer
    {
        std::function<void(P...)> func;

        template <typename T>
        EventObserver(T &&func) : func(std::forward<T>(func)) {}

        void Observe(const Event &e) const override
        {
            std::apply(func, dynamic_cast<const BasicEvent &>(e).params);
        }
    };

    // We need a protected destructor, but adding one silently removes the move operations.
    // And adding the move operations removes the copy operations, so we add those too.
    BasicEvent(const BasicEvent &) = default;
    BasicEvent(BasicEvent &&) = default;
    BasicEvent &operator=(const BasicEvent &) = default;
    BasicEvent &operator=(BasicEvent &&) = default;

  protected:
    ~BasicEvent() {}
};

class EventDispatcher
{
  public:
    template <typename E>
    void Subscribe(typename E::EventObserver observer)
    {
        event_subscribers.insert_or_assign(typeid(E), std::make_unique<typename E::EventObserver>(std::move(observer)));
    }

    template <typename E>
    void QueueEvent(E &&event)
    {
        event_queue.push(std::make_unique<std::remove_cvref_t<E>>(std::forward<E>(event)));
    }

    void DispatchQueue()
    {
        while (!event_queue.empty())
        {
            Event &event = *event_queue.front();
            event_subscribers.at(typeid(event))->Observe(event);
            event_queue.pop();
        }
    }

  private:
    std::queue<std::unique_ptr<Event>> event_queue;
    std::map<std::type_index, std::unique_ptr<Observer>> event_subscribers;
};


struct EventA : BasicEvent<>         {using BasicEvent::BasicEvent;};
struct EventB : BasicEvent<>         {using BasicEvent::BasicEvent;};
struct EventC : BasicEvent<int, int> {using BasicEvent::BasicEvent;};

int main()
{
    EventDispatcher dis;
    dis.Subscribe<EventA>([]{std::cout << "Observing A!\n";});
    dis.Subscribe<EventB>([]{std::cout << "Observing B!\n";});
    dis.Subscribe<EventC>([](int x, int y){std::cout << "Observing C: " << x << ", " << y << "!\n";});
    dis.QueueEvent(EventA());
    dis.QueueEvent(EventB());
    dis.QueueEvent(EventC(1, 2));
    dis.DispatchQueue();
}
3赞 joergbrech 11/12/2022 #3

您可以创建自己的版本,该版本使用类型擦除接受任何签名的功能。不过,这需要一些繁重的工作。我将为需要 C++17 的 void 函数提供一个解决方案,因为我们将使用 .std::functionstd::any

我将首先引导您完成这些步骤,然后在代码中提供完整的解决方案。

  • 首先,使用模板元编程创建一些函数来捕获任何类型函数的参数的数量和类型。我们可以从这里“借用”。function_traits
  • 然后,创建一个具有模板化调用运算符的类。VariadicVoidFunction
  • 此调用运算符创建一个并将其传递给 的成员的方法,该成员是 类型的(拥有资源的智能)指针。std::vector<std::any>invokeVariadicVoidFunctionVariadicVoidFunction::Concept
  • VariadicVoidFunction::Concept是一个抽象基类,其虚拟方法接受 。invokestd::vector<std::any>
  • VariadicVoidFunction::Function是一个类模板,其中 template 参数是一个函数。它将此函数存储为成员并继承。它实现该方法。在这里,我们可以将向量元素返回到预期的类型,我们可以使用 .这允许我们使用正确的参数类型调用实际函数。VariadicVoidFunction::Conceptinvokestd::any_castfunction_traits
  • VariadicVoidFunction获取接受任何类型的函数的模板化构造函数。它创建一个类型的实例,并将其存储在一个拥有的(智能)指针中。FVariadicVoidFunction::Function<F>
#include <memory>
#include <vector>
#include <any>

// function_traits and specializations are needed to get arity of any function type
template<class F>
struct function_traits;

// ... function pointer
template<class R, class... Args>
struct function_traits<R(*)(Args...)> : public function_traits<R(Args...)>
{};

// ... normal function
template<class R, class... Args>
struct function_traits<R(Args...)>
{
    static constexpr std::size_t arity = sizeof...(Args);

    template <std::size_t N>
    struct argument
    {
        static_assert(N < arity, "error: invalid parameter index.");
        using type = typename std::tuple_element<N,std::tuple<Args...>>::type;
    };
};

// ... non-const member function
template<class C, class R, class... Args>
struct function_traits<R(C::*)(Args...)> : public function_traits<R(C&,Args...)>
{};

// ... const member function
template<class C, class R, class... Args>
struct function_traits<R(C::*)(Args...) const> : public function_traits<R(C const&,Args...)>
{};

// ... functor (no overloads allowed)
template<class F>
struct function_traits
{
    private:
        using call_type = function_traits<decltype(&F::operator())>;
    public:
        static constexpr std::size_t arity = call_type::arity - 1;

        template <std::size_t N>
        struct argument
        {
            static_assert(N < arity, "error: invalid parameter index.");
            using type = typename call_type::template argument<N+1>::type;
        };
};

template<class F>
struct function_traits<F&> : public function_traits<F>
{};

template<class F>
struct function_traits<F&&> : public function_traits<F>
{};

// type erased void function taking any number of arguments
class VariadicVoidFunction
{
public:

    template <typename F>
    VariadicVoidFunction(F const& f)
     : type_erased_function{std::make_shared<Function<F>>(f)} {}

    template <typename... Args>
    void operator()(Args&&... args){
        return type_erased_function->invoke(std::vector<std::any>({args...}));
    }

private:

    struct Concept {
        virtual ~Concept(){}
        virtual void invoke(std::vector<std::any> const& args) = 0;
    };

    template <typename F>
    class Function : public Concept
    {
    public:
        Function(F const& f) : func{f} {}

        void invoke(std::vector<std::any> const& args) override final
        {
            return invoke_impl(
                args, 
                std::make_index_sequence<function_traits<F>::arity>()
            );
        }
    private:

        template <size_t... I>
        void invoke_impl(std::vector<std::any> const& args, std::index_sequence<I...>)
        {
            return func(std::any_cast<typename function_traits<F>::template argument<I>::type>(args[I])...);
        }

        F func;
    };

    std::shared_ptr<Concept> type_erased_function;

};

你可以像这样使用它:

#include <unordered_map>
#include <iostream>

int main()
{
    VariadicVoidFunction([](){});

    std::unordered_map<size_t, VariadicVoidFunction> map = 
        {
            {0, VariadicVoidFunction{[](){ std::cout << "no argument\n";  }} },
            {1, VariadicVoidFunction{[](int i){ std::cout << "one argument\n"; }} },
            {2, VariadicVoidFunction{[](double j, const char* x){ std::cout<< "two arguments\n"; }} }
        };
    
    map.at(0)();
    map.at(1)(42);
    map.at(2)(1.23, "Hello World");

    return 0;
}
no argument
one argument
two arguments

Godbolt 编译器资源管理器演示

请注意,这是帮助您入门的典型解决方案。一个缺点是所有参数都会被复制到 您可以通过传递指向 std::any 的指针来避免这种情况,但是在执行此操作时必须注意生存期。std::any

评论

0赞 joergbrech 11/12/2022
在侧节点上,对于名称来说是一个糟糕的选择,因为它不是真正的可变参数(参数的数量必须与传递给 ctor 的函数的参数数量匹配),并且您不能对它使用可变参数函数(type_traits不够强大)。只能对任意函数进行建模,每个函数具有固定数量的参数,每个参数都属于同一类型。VariadicVoidFunction
1赞 F4LS3 11/12/2022 #4

在@joergbrech和@HolyBlackCat的输入之后,我做了这个

enum class EventType {
    WindowClosed, WindowResized, WindowFocused, WindowLostFocus, WindowMoved,
    AppTick, AppUpdate, AppRender,
    KeyPressed, KeyRelease,
    MouseButtonPressed, MouseButtonRelease, MouseMoved, MouseScrolled,
    ControllerAxisChange, ControllerButtonPressed, ControllerConnected, ControllerDisconnected
};

class IEvent {
public:
    IEvent(EventType event_type) {
        this->event_type = event_type;
    }

    EventType get_event_type() {
        return event_type;
    }

private:
    EventType event_type;
};

class IEventSubscriber {
public:
    /**
     * @param event The event that is passed to the subscriber by the publisher; should be cast to specific event
     * */
    virtual void on_event(IEvent *event) = 0;

    EventType get_event_type() {
        return event_type;
    }

protected:
    explicit IEventSubscriber(EventType event_type) {
        this->event_type = event_type;
    }

private:
    EventType event_type;
};

class FORGE_API EventPublisher {
public:
    static void subscribe(IEventSubscriber *subscriber);
    static void queue_event(IEvent *event);
    static void dispatch_queue();

private:
    static std::queue<IEvent*> event_queue;
    static std::set<IEventSubscriber*> event_subscribers;
};

我已经测试了它,我从这个解决方案中得到了预期的结果。对于完整的代码解决方案->https://github.com/F4LS3/forge-engine