【问题标题】:Store a function with arbitrary arguments and placeholders in a class and call it later将具有任意参数和占位符的函数存储在一个类中并稍后调用它
【发布时间】:2017-10-31 08:13:27
【问题描述】:

因此,如果您愿意,我正在创建一种事件处理程序,并且正在编写“事件侦听器包装器”。

基本思想是这样的: 当你想订阅一个事件时,你创建一个在事件触发时应该调用的函数。

您将此侦听器函数放入一个包装器中,以将该函数传递给调度程序。

调度程序获取一个事件,为您的侦听器找到包装器,并使用事件设置的参数值调用底层函数。

只要听众都只接受我的EventBase 类的一个参数,我已经有了一些工作。然后我必须将它输入到传递侦听器的正确事件中。

我想要的是让我的侦听器函数具有“任何”类型的参数,并以一种允许我根据触发的事件使用我想要的任何参数调用它的方式存储函数。每个侦听器函数只会接收一种类型的事件,或者它本身的事件。这将允许我不必在每个侦听器中键入强制转换每个事件,而是会传递正确的事件。

我为这个包装器找到了一些几乎完美的代码,但有一些我似乎无法修复的小问题。我会在下面解释。

@hmjd 编写的代码:

#include <iostream>
#include <string>
#include <functional>
#include <memory>

void myFunc1(int arg1, float arg2)
{
    std::cout << arg1 << ", " << arg2 << '\n';
}
void myFunc2(const char *arg1)
{
    std::cout << arg1 << '\n';
}

class DelayedCaller
{
public:
    template <typename TFunction, typename... TArgs>
    static std::unique_ptr<DelayedCaller> setup(TFunction&& a_func,
                                                TArgs&&... a_args)
    {
        return std::unique_ptr<DelayedCaller>(new DelayedCaller(
            std::bind(std::forward<TFunction>(a_func),
                      std::forward<TArgs>(a_args)...)));
    }
    void call() const { func_(); }

private:
    using func_type = std::function<void()>;
    DelayedCaller(func_type&& a_ft) : func_(std::forward<func_type>(a_ft)) {}
    func_type func_;
};

int main()
{
    auto caller1(DelayedCaller::setup(&myFunc1, 123, 45.6));
    auto caller2(DelayedCaller::setup(&myFunc2, "A string"));

    caller1->call();
    caller2->call();

    return 0;
}

我在这里做的第一件事是我必须用std::shared_ptr 替换std::unique_ptr。不知道为什么真的。这几乎可以工作。在我的用例中,我需要存储一个方法函数(意味着绑定需要传递包含方法对象?),并且在存储函数时我不知道参数值是什么,这就是事件来决定。所以我的调整如下:

class DelayedCaller
{
public:

    template <typename TFunction, typename TClass>
    static std::shared_ptr<DelayedCaller> setup(TFunction&& a_func,
                                                TClass && a_class)
    {

        auto func = std::bind(std::forward<TFunction>(a_func),
                              std::forward<TClass>(a_class),
                              std::placeholders::_1);

        return std::shared_ptr<DelayedCaller>(new DelayedCaller(func));
    }

    template <typename T>
    void call( T v ) const { func_(v); }

private:
    using func_type = std::function<void(  )>;
    DelayedCaller(func_type&& a_ft) : func_(std::forward<func_type>(a_ft)) {}
    func_type func_;
};

为了测试,我删除了参数包,并将其替换为持有该函数的类对象的直接参数。我还为绑定提供了 1 个参数的占位符(理想情况下,稍后由 void call() 函数替换)。

它是这样创建的:

eventManager->subscribe(EventDemo::descriptor, DelayedCaller::setup(
                                &AppBaseLogic::getValueBasic,
                                this
                                ));

问题是:在这一行:

return std::shared_ptr<DelayedCaller>(new DelayedCaller(func));

我得到“没有匹配函数调用'DelayedCaller::DelayedCaller(std::_Bind(AppBaseLogic*, std::_Placeholder)>&)' return std::shared_ptr(new DelayedCaller(func));"

这只发生在使用placeholder::_1 时。如果我用正确类型的已知值替换它,它就可以工作,除了函数在没有任何有用数据的情况下被调用。

所以,我想我需要一种方法来使用我不知道类型的占位符来存储函数?

如果我把事情的名字弄错了,请原谅我。我对c++很陌生,最近几天才开始学习。

**编辑:**

好的,所以我只是在更新为什么我需要存储这样的函数。 我的事件调度程序中有一个如下所示的地图:

std::map< const char*, std::vector<DelayedCaller> > _observers;

我希望能够像这样调用“延迟调用者”中的函数:

void Dispatcher::post( const EventBase& event ) const
{
    // Side Note: I had to do this instead of map.find() and map.at() because 
    // passing a "const char*" was not evaluating as equal to event.type() even 
    // though event.type() is also a const char*. So instead I am checking it 
    // myself, which is fine because it gives me a little more control.

    std::string type(event.type());
    for( auto const &x : _observers ) {
        std::string type2(x.first);
        if ( type == type2 ) {
            auto&& observers = x.second;

            for( auto&& observer : observers ) {
                // event may be any descendant of EventBase.
                observer.slot->call(event);
            }
            break;
        }
    }
}

我的听众目前看起来像这样:

void AppBaseLogic::getValue(const EventBase &e) {
    const EventDemo& demoEvent = static_cast<const EventDemo&>( e );
    std::cout << demoEvent.type();
}

我正在尝试存储每个函数,以便参数看起来像这样:

void AppBaseLogic::getValue(const EventAnyDescendant &e) {
    std::cout << e.type();
}

希望这会有所帮助。感谢大家花时间帮助我。

关于 lambda 的旁注:有人建议使用它们,我知道它们是什么或如何使用它们,但我将对它们进行一些研究,看看是否更有意义。尽管从我所看到的情况来看,我担心它们的可维护性。

【问题讨论】:

  • 嗯,为什么不单独使用std::function
  • 我之前问过的类似问题stackoverflow.com/questions/43396129/…
  • “让我根据触发的事件使用我想要的任何参数调用它”。这就是重点:每个事件都需要一个特定的签名,因此请按事件类型创建函数列表std::vector&lt;std::function&lt;void(intx, int y)&gt;&gt; onClick; std::vector&lt;std::function&lt;void(char)&gt;&gt; onKeyPressed;...
  • 为每种类型的事件创建不同向量的想法“可能”奏效。我将不得不做一些截然不同的事情。现在事件存储在地图中,通过“event_Key”:要调用的函数向量。这样,如果发出事件并且没有任何侦听器,则无关紧要。我真的不知道如何在添加侦听器时自动动态地创建一个新的向量列表,但我可能能够做到。另一个问题的一部分是我想让一些事件模板化。因此 EventMsg、EventMsg、EventMsg ... 的列表不同

标签: c++ c++11 variadic-templates variadic stdbind


【解决方案1】:

您的DelayedCaller 在做什么还不是很清楚。如果你重构代码并去掉它,你会得到这样的:

auto c1 = []() {myFunc1(123, 45.6);}; // or use bind, the result is exactly the same
auto c2 = []() {myFunc2("A string");};

vector<function<void()>> v {c1, c2};
v[0]();
v[1](); // ok

现在,如果您尝试在此版本中引入占位符修改,那么一开始它为什么不起作用就很清楚了:

auto cSome = [](???) {getValueBasic(???)};

你用什么替换???

getValueBasic 接受某种特定类型的参数,它会泄漏到cSome 签名中。无论您将其包装在多少个模板包装器中,它都会泄漏到每个包装器的签名中,直到并包括最外层的包装器。 bindstd::placeholders 不是能够让它发生的魔杖。

换句话说,如果你不知道你的函数的类型,你就不能调用它(很明显,不是吗?)

对签名进行类型擦除并使所有可调用对象符合相同类型的一种方法是在运行时(又名dynamic_cast)对它们进行类型检查和类型转换。另一种是双重派送。这两种方法都是visitor 相同的一般思想的不同体现。谷歌“访问者模式”了解更多信息。

【讨论】:

  • 感谢您的澄清。正如我在问题中提到的,我才学习 c++ 几天。对于大多数应用程序开发人员来说,我拥有丰富的 Web 开发背景和 Java。试图学习新的东西。我不习惯在不知道参数是什么的情况下无法将函数作为回调传递。说“在映射中存储对这个任意函数的引用,然后稍后使用这些参数调用它。使用不同的参数从映射调用不同的函数”似乎很简单。但是地图只能存储一种类型。这对我来说很奇怪。
  • @DalenCatt 这是给你的静态类型。假设您可以将具有不同签名的函数存储在同一个映射中。假设现在您获取一个函数,该函数需要一个指向键盘事件的指针,并将一个包含 3 个双精度数的数组传递给它。你期望会发生什么?实现应该如何处理?
  • 我想我在这个用例中的期望是,如果你关注你正在做的事情,那将不会发生。在这种情况下,因为它在调用函数之前检查事件的类型,然后传递相同的事件,这应该不是问题,但是是的,我理解为什么在正常情况下它不起作用。我想我只是希望语言更能相信我知道我想做什么。我觉得这是限制性的。
  • @DalenCatt 它能够信任您,但您需要非常明确地说明这一点。要在 C++ 中说“相信我,我知道我在做什么”,你可以使用类型转换,特别是 reinterpret_cast。我强烈反对它。如果你不得不问如何做一件危险的事情,你不应该这样做。
  • 对,我理解你传递完全不同的变量类型。但我所说的是传递一个基类的孩子。并将具有相同“结构”的函数存储在一个数组/向量中,只是将基类的不同子类存储在一个数组/向量中。在我看来,必须有某种方法可以做到这一点。它可能比我现在的水平(显然)更先进,但这个概念似乎很简单。
【解决方案2】:

也许这适合你。使用 c++11

#include <iostream>                                                                                                                                                                                                 
#include <functional>
#include <vector>

namespace test
{


  std::vector<std::function<void()>> listeners;

  template<typename F, typename... Args>
  void add_listener(F call, Args&& ...args )
  {   
    std::cout << "callback_dispatcher>" << __PRETTY_FUNCTION__ << "enter <<< " << std::endl;                                                 
    auto invoke_me = [=]()mutable{
      call(std::move(args)...);
    };  
    listeners.push_back(invoke_me);
  }   

  void dispatch_all()
  {
    for(auto func: listeners)
    {   
       func();
    }   
  }   

}

int main()
{
  std::cout << "Main entered..." << std::endl;


  test::add_listener(
    [](int a)
    {   
      std::cout << "void(int) lambda dispatched with a = " << a << std::endl;
    },  
    5   
  );  
  test::add_listener(
    [](int a, std::string str)
    {   
      std::cout << "void(int, string) lambda dispatched with a = " << a << ", str = " << str << std::endl;
    },  
    10, "Hello World!"
  );  

  test::dispatch_all();

  std::cout << "Main exited..." << std::endl;
}

输出:

Main entered...
callback_dispatcher>void test::add_listener(F, Args&& ...) [with F = main()::<lambda(int)>; Args = {int}]enter <<< 
callback_dispatcher>void test::add_listener(F, Args&& ...) [with F = main()::<lambda(int, std::__cxx11::string)>; Args = {int, const char (&)[13]}]enter <<< 
void(int) lambda dispatched with a = 5
void(int, string) lambda dispatched with a = 10, str = Hello World!
Main exited...

请参阅SO_QUESTION,了解为什么在 lambda 中扩展 args 时使用 mutable 和 std::move。

【讨论】:

  • 哇,没想到反应这么快。我要去睡觉了,哈哈。我不熟悉 lambda,也不确定它们是否能在这种情况下工作。这也适用于占位符吗?另外,这是代替调用成员函数吗?我想我需要了解 lambdas 是如何工作的。
  • 这并不能解决任何问题。所有参数都在调用add_listener 的地方提供。你也可以在 lambdas 中捕获它们。这与第一个示例相同。在实际情况下,直到调用 dispatch() 时参数才可用。
【解决方案3】:

看看 std::bind 或许还有 std::mem_fn

c+=11 版本能够对参数列表进行各种巧妙的转换,以生成类似函数的对象。

当然,Lambda 提供了更大的灵活性,而且大多数情况下您可以混合使用它们。

【讨论】:

    【解决方案4】:

    我在DelayedCaller 的修改(方法和占位符)版本中发现了两个主要问题

    (1) 现在call() 接收一个参数(T 类型),所以func_() 用一个参数调用;但是func_()仍然定义为std::function&lt;void()&gt;类型,所以无法接收参数[这点是你的“没有匹配函数”错误的原因]

    (2) 如果模板化call(),接收到T类型的参数,还需要模板化func_的类型,变成std::function&lt;void(T)&gt;;所以你必须模板化整个类。

    考虑到计数 (1) 和 (2),并保持 std::unique_ptr,我已将您的 DelayedCaller 重写为 dcM1M 用于“方法”,1 用于“1 参数”)

    template <typename T>
    class dcM1
     {
       public:
          template <typename TFunction, typename TClass>
          static std::unique_ptr<dcM1> setup (TFunction && a_func,
                                              TClass && a_class)
           {
             auto func = std::bind(std::forward<TFunction>(a_func),
                                   std::forward<TClass>(a_class),
                                   std::placeholders::_1);
    
             return std::unique_ptr<dcM1>(new dcM1(func));
           }
    
          void call( T v ) const
           { func_(v); }
    
       private:
          using func_type = std::function<void(T)>;
    
          dcM1(func_type && a_ft) : func_(std::forward<func_type>(a_ft))
           { }
    
          func_type func_;
     };
    

    并且可以如下使用

       auto cm1f = dcM1<int>::setup(&foo::func, &f);
       auto cm1b = dcM1<long>::setup(&bar::func, &b);
    
       cm1f->call(0);
       cm1b->call(1L);
    

    以下是一个完整的工作示例

    #include <iostream>
    #include <string>
    #include <functional>
    #include <memory>
    
    void myFunc1 (int arg1, float arg2)
     { std::cout << arg1 << ", " << arg2 << '\n'; }
    
    void myFunc2 (char const * arg1)
     { std::cout << arg1 << '\n'; }
    
    class dcVoid
     {
       public:
          template <typename TFunction, typename... TArgs>
          static std::unique_ptr<dcVoid> setup (TFunction && a_func,
                                                       TArgs && ... a_args)
           {
             return std::unique_ptr<dcVoid>(new dcVoid(
                   std::bind(std::forward<TFunction>(a_func),
                             std::forward<TArgs>(a_args)...)));
           }
    
          void call() const
           { func_(); }
    
       private:
          using func_type = std::function<void()>;
    
          dcVoid(func_type && a_ft) : func_(std::forward<func_type>(a_ft))
           { }
    
          func_type func_;
     };
    
    template <typename T>
    class dcM1
     {
       public:
          template <typename TFunction, typename TClass>
          static std::unique_ptr<dcM1> setup (TFunction && a_func,
                                              TClass && a_class)
           {
             auto func = std::bind(std::forward<TFunction>(a_func),
                                   std::forward<TClass>(a_class),
                                   std::placeholders::_1);
    
             return std::unique_ptr<dcM1>(new dcM1(func));
           }
    
          void call( T v ) const
           { func_(v); }
    
       private:
          using func_type = std::function<void(T)>;
    
          dcM1(func_type && a_ft) : func_(std::forward<func_type>(a_ft))
           { }
    
          func_type func_;
     };
    
    struct foo
     { void func (int i) { std::cout << "foo func: " << i << std::endl; } };
    
    struct bar
     { void func (long l) { std::cout << "bar func: " << l << std::endl; } };
    
    int main ()
     {
       auto cv1 = dcVoid::setup(&myFunc1, 123, 45.6);
       auto cv2 = dcVoid::setup(&myFunc2, "A string");
    
       foo f;
       bar b;
    
       auto cm1f = dcM1<int>::setup(&foo::func, &f);
       auto cm1b = dcM1<long>::setup(&bar::func, &b);
    
       cv1->call();
       cv2->call();
    
       cm1f->call(0);
       cm1b->call(1L);
     }
    

    【讨论】:

    • 所以你理解了我的问题。你对我为什么收到错误是正确的,我应该更清楚我理解这一点。问题是我需要将这些任意函数存储在稍后调用的映射中。即:std::map>>。但这不能存储模板类,因为它们将是不同的类型。然后我会使用某种形式的 map.at("someindex")->iterate_through_vector_to_object ->call("some", "args");这有意义吗?
    • @DalenCatt - 我明白了......我能想象的最好的事情是为不同的DelayedCaller 模板类使用一个通用的基类(比如dcBase)并存储一个智能指针向量给dcBase;类似std::map&lt;const char*, std::vector&lt;std::unique_ptr&lt;dcBase&gt;&gt;&gt;
    • @DalenCatt - 基类的问题是您可以使用虚函数,但虚函数不能是模板。所以你可以用call()做这个,当调用没有参数时,但不能用template &lt;typename T&gt; call(T t),因为不能被虚拟化;所以我认为只有在基类中修复一组(或有限的一组)可能的“某些”“args”类型时,这才有效。
    • 这行不通,因为传递给 call 的参数将是 EventBase 的任何子级。但它永远不会是 EventBase。如果我将参数写为 EventBase,并将其传递给 EventDemo,那么它会抱怨它的类型错误。
    • 而且我也无法存储使用 std::function 甚至子类以外的任何类型的函数。这对我来说很奇怪。我觉得它应该足够聪明来存储子类。
    【解决方案5】:

    好的,所以我知道这已经有一段时间了。我一直在对不同的事件模式进行大量研究,试图找到更接近我所追求的东西。在倾注了所有内容之后,在那些离开 cmets 的人的建议下,我决定使用信号/插槽模式,这可能是 C++ 中使用最广泛的事件模式。接近它的方法是让我的所有“逻辑类”(无论是用于 gui 还是用于计算)都保留对第三个“信号事件持有者类”的引用,为简单起见,我将其称为事件代理。这和我能得到的一样好。您可能希望拥有的任何事件都可以添加到此类中,并且可以从任何引用事件代理的类中访问和调用它。我发现了一个由Simon Schneegans 制作的非常好的信号类,但我正在积极尝试寻找/学习如何让事情变得更好(线程安全,也许更快?)。如果有人像我一样有兴趣/寻求帮助,你可以在这里找到我的超级基本测试用例: https://github.com/Moonlight63/QtTestProject

    谢谢!

    【讨论】:

      猜你喜欢
      • 2015-02-12
      • 1970-01-01
      • 1970-01-01
      • 2017-08-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多