将具有任意参数和占位符的函数存储在类中,然后再调用它

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

本文关键字:然后 调用 存储 函数 任意 参数 占位符      更新时间:2023-10-16

所以我正在创建一种类型的事件处理程序,如果你愿意的话,我正在编写一个"事件侦听器包装器"。

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

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

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

只要听众都只接受我的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_;
};

为了进行测试,我删除了参数包,并将其替换为持有该函数的类对象的直接参数。我还为bind提供了一个1参数的占位符(最好稍后用void call()函数替换)。

它是这样创建的:

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

问题是:在这条线上:

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

我得到"没有用于调用'DelayedCaller::DelayedColler(std::_Bind(AppBaseLogic*,std::_Placeholder<1>)>&'的匹配函数"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();
}

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

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

不太清楚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签名中。无论你用多少个模板包装器包装它,它都会泄漏到每个包装器的签名中,直到最外面的一个。CCD_ 10和CCD_。

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

键入擦除签名并使所有可调用程序符合同一类型的一种方法是在运行时进行类型检查并对其进行类型转换(也称为dynamic_cast)。另一种是双重派遣。这两种方法都是访问者相同总体思想的不同体现。谷歌"访客模式"获取更多信息。

也许这很适合你。使用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时使用可变和std::move的原因。

看看std::bind,也许还有std::mem_fn

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

当然,Lambdas提供了更大的灵活性,你可以将它们混合在一起。

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

(1) 现在call()接收到一个参数(类型为T),因此用一个参数调用func_();但是func_()仍然定义为std::function<void()>类型,因此无法接收参数[这一点是您的"无匹配功能"错误的原因]

(2) 如果模板化call(),接收类型为T的参数,则还需要模板化成为std::function<void(T)>func_的类型;所以您必须将整个类模板化。

考虑到计数(1)和(2),并保持std::unique_ptr,我将您的DelayedCaller重写为dcM1(M表示"方法",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);
}

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

谢谢!

相关文章: