将具有任意参数和占位符的函数存储在 class 中,稍后调用

Store a function with arbitrary arguments and placeholders in a class and call it later

所以我正在创建一种事件处理程序,如果您愿意的话,我正在编写一个 "Event Listener Wrapper"。

基本思路是这样的: 当您想要订阅一个事件时,您可以创建一个函数,该函数应在事件触发时调用。 <-- 已经完成了(有点,我会解释)

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

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

只要听众只接受我的 EventBase class 的一个论点,我就已经有了一些工作。然后我必须将其强制转换为传递侦听器的正确事件。

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

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

@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_;
};

为了测试,我删除了参数包并将其替换为 class 持有该函数的对象的直接参数。我还为绑定提供了 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<1>)>&)' return std::shared_ptr(新的延迟调用者(函数));

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

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

如果我把事物的名称弄错了,请原谅我。我是c++新手,这几天才开始学。

**编辑:**

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

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

我希望能够像这样调用 "Delayed Caller" 中的函数:

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 的旁注:有人建议使用它们,我知道它们是什么或如何使用它们,但我打算对它们进行一些研究,看看这是否更有意义。从我所看到的情况来看,我担心他们的可维护性。

这可能适合你。使用 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...

参考 了解为什么在 lambda 中扩展参数时使用可变和 std::move。

看看 std::bind 也许 std::mem_fn

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

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

我发现 DelayedCaller

的修改(方法和占位符)版本存在 2 个主要问题

(1) 现在 call() 收到一个参数(T 类型)所以 func_() 被调用时带有一个参数;但是 func_() 仍然定义为 std::function<void()> 类型,所以无法接收参数 [这一点是 "no matching function" 错误的原因]

(2) 如果模板化 call(),接收类型为 T 的参数,则有必要模板化 func_ 的类型成为 std::function<void(T)>;所以你必须模板化完整的 class.

考虑到计数 (1) 和 (2),并保持 std::unique_ptr,我已将您的 DelayedCaller 重写为 dcM1M for "method" 和 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);
 }

不太清楚您的 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 不是能够让它不发生的魔杖。

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

一种类型擦除签名并使所有可调用对象符合相同类型的方法是在运行时间对它们进行类型检查和类型转换(a.k.a. dynamic_cast).另一种是双重派遣。这两种方法都是 visitor 相同总体思想的不同体现。 Google "the visitor pattern" 了解更多信息。

好的,我知道这已经有一段时间了。我一直在对不同的事件模式进行大量研究,试图找到更接近我所追求的东西。在仔细研究了所有内容之后,根据那些在这里留下评论的人的建议,我决定使用 Signal/Slot 模式,这可能是 C++ 中使用最广泛的事件模式。接近它的方法是让我的所有 "logic classes"(无论是用于 gui 还是用于计算)保留对第三个 "signal event holder class" 的引用,为简单起见,我将其称为事件代理。这和我能得到的一样好。您可能想要的任何事件都可以添加到此 class,并且可以通过引用事件代理从任何 class 访问和调用它。我发现 Simon Schneegans 发出了一个很好的信号 class,但我正在积极尝试 find/learn 如何做得更好(线程安全,也许更快?)。如果有人像我一样 interested/looking 寻求帮助,你可以在这里找到我的超级基本测试用例: https://github.com/Moonlight63/QtTestProject

谢谢!