好的,我最近发布了一些与使用 C++11-ish 接口包装 C 回调 API 相关的问题。我几乎得到了一个令人满意的解决方案,但我认为它可能更优雅,需要一些模板元编程向导的帮助:)
请耐心等待,因为示例代码有点长,但我尝试一次性演示该问题。基本上,这个想法是,给定一个函数指针和数据上下文指针列表,我想提供一个可以提供的回调机制,
- 函数指针
- 函数对象(函子)
- 拉姆达斯
此外,我想让这些函数可以被各种原型调用。我的意思是,C API 为回调提供了大约 7 个不同的参数,但在大多数情况下,用户代码实际上只对其中的一两个感兴趣。所以我希望用户能够只指定他感兴趣的参数。(这从首先允许 lambdas 的角度延伸......为了简洁。)
在这个例子中,名义上的 C 回调接受一个int
和一个float
参数,以及一个float*
可用于返回一些额外数据的可选参数。因此,C++ 代码的目的是能够以“可调用”的任何形式提供任何这些原型的回调。(例如函子、lambda 等)
int callback2args(int a, float b);
int callback3args(int a, float b, float *c);
到目前为止,这是我的解决方案。
#include <cstdio>
#include <vector>
#include <functional>
typedef int call2args(int,float);
typedef int call3args(int,float,float*);
typedef std::function<call2args> fcall2args;
typedef std::function<call3args> fcall3args;
typedef int callback(int,float,float*,void*);
typedef std::pair<callback*,void*> cb;
std::vector<cb> callbacks;
template <typename H>
static
int call(int a, float b, float *c, void *user);
template <>
int call<call2args>(int a, float b, float *c, void *user)
{
call2args *h = (call2args*)user;
return (*h)(a, b);
}
template <>
int call<call3args>(int a, float b, float *c, void *user)
{
call3args *h = (call3args*)user;
return (*h)(a, b, c);
}
template <>
int call<fcall2args>(int a, float b, float *c, void *user)
{
fcall2args *h = (fcall2args*)user;
return (*h)(a, b);
}
template <>
int call<fcall3args>(int a, float b, float *c, void *user)
{
fcall3args *h = (fcall3args*)user;
return (*h)(a, b, c);
}
template<typename H>
void add_callback(const H &h)
{
H *j = new H(h);
callbacks.push_back(cb(call<H>, (void*)j));
}
template<>
void add_callback<call2args>(const call2args &h)
{
callbacks.push_back(cb(call<call2args>, (void*)h));
}
template<>
void add_callback<call3args>(const call3args &h)
{
callbacks.push_back(cb(call<call3args>, (void*)h));
}
template<>
void add_callback<fcall2args>(const fcall2args &h)
{
fcall2args *j = new fcall2args(h);
callbacks.push_back(cb(call<fcall2args>, (void*)j));
}
template<>
void add_callback<fcall3args>(const fcall3args &h)
{
fcall3args *j = new fcall3args(h);
callbacks.push_back(cb(call<fcall3args>, (void*)j));
}
// Regular C-style callback functions (context-free)
int test1(int a, float b)
{
printf("test1 -- a: %d, b: %f", a, b);
return a*b;
}
int test2(int a, float b, float *c)
{
printf("test2 -- a: %d, b: %f", a, b);
*c = a*b;
return a*b;
}
void init()
{
// A functor class
class test3
{
public:
test3(int j) : _j(j) {};
int operator () (int a, float b)
{
printf("test3 -- a: %d, b: %f", a, b);
return a*b*_j;
}
private:
int _j;
};
// Regular function pointer of 2 parameters
add_callback(test1);
// Regular function pointer of 3 parameters
add_callback(test2);
// Some lambda context!
int j = 5;
// Wrap a 2-parameter functor in std::function
add_callback(fcall2args(test3(j)));
// Wrap a 2-parameter lambda in std::function
add_callback(fcall2args([j](int a, float b)
{
printf("test4 -- a: %d, b: %f", a, b);
return a*b*j;
}));
// Wrap a 3-parameter lambda in std::function
add_callback(fcall3args([j](int a, float b, float *c)
{
printf("test5 -- a: %d, b: %f", a, b);
*c = a*b*j;
return a*b*j;
}));
}
int main()
{
init();
auto c = callbacks.begin();
while (c!=callbacks.end()) {
float d=0;
int r = c->first(2,3,&d,c->second);
printf(" result: %d (%f)\n", r, d);
c ++;
}
}
好的,如您所见,这确实有效。但是,我发现必须将仿函数/lambda 显式包装为std::function
不优雅的类型的解决方案。我真的很想让编译器自动匹配函数类型,但这似乎不起作用。如果我删除 3 参数变体,则不需要fcall2args
包装器,但是 的版本的存在使得编译器显然不明确。换句话说,它似乎无法基于 lambda 调用签名进行模式匹配。fcall3args
add_callback
第二个问题是,我当然是使用 复制仿函数/lambda 对象new
,但不使用delete
此内存。我目前不确定跟踪这些分配的最佳方法是什么,尽管我想在实际的实现中我可以在一个对象中跟踪它们add_callback
,并在析构函数中释放它们。
第三,对于我想要允许的回调的每个变体,我不觉得有特定的 types 等不是很优雅call2args
。call3args
这意味着对于用户可能需要的每种参数组合,我都需要大量类型。我希望可以有一些模板解决方案来使它更通用,但我很难想出它。
编辑解释:此代码中的定义std::vector<std::pair<callback*,void*>> callbacks
是问题定义的一部分,而不是答案的一部分。我试图解决的问题是将 C++ 对象映射到这个接口上——因此,提出更好的组织方法std::vector
并不能解决我的问题。谢谢。只是为了澄清。
编辑#2std::vector<std::pair<callback*,void*>> callbacks
:好的,忘记我的示例代码用于保存回调的事实。相反,想象一下,因为这是实际场景,我有一些 C 库实现以下接口:
struct someobject *create_object();
free_object(struct someobject *obj);
add_object_callback(struct someobject *obj, callback *c, void *context);
在哪里callback
,
typedef int callback(int a,float b,float *c, void *context);
好的。所以“someobject”会经历某种外部事件、网络数据或输入事件等,并在这些发生时调用它的回调列表。
这是 C 中回调的一个非常标准的实现。重要的是,这是一个现有的库,我无法更改它,但我正在尝试围绕它编写一个很好的、惯用的 C++ 包装器。我希望我的 C++ 用户能够将 lambdas 添加为回调。因此,我想设计一个 C++ 接口,让用户能够执行以下操作:
add_object_callback(struct someobject *obj, func);
其中func
是以下之一:
- 一个不使用的常规 C 函数
context
。 - 函子对象
- 一个拉姆达
此外,在每种情况下,函数/函子/lambda 应该有可能具有以下任一签名:
int cb2args(int a, float b);
int cb2args(int a, float b, float *c);
我认为这应该是可能的,我已经完成了大约 80% 的工作,但我被困在基于调用签名的模板多态性上。我不知道这是否可能。也许它需要一些涉及巫毒的function_traits
东西,但这有点超出我的经验。无论如何,有很多很多的 C 库使用这样的接口,我认为在 C++ 中使用它们时允许这种便利性会很棒。