【发布时间】:2013-08-12 17:07:06
【问题描述】:
假设这是一个要包装的 C 函数:
void foo(int(__stdcall *callback)());
C 函数指针回调的两个主要缺陷是:
- 无法存储绑定表达式
- 无法存储捕获的 lambda
我想知道包装此类函数的最佳方法。第一个对于成员函数回调特别有用,第二个对于使用周围变量的内联定义特别有用,但这些并不是唯一的用途。
这些特定函数指针的另一个属性是它们需要使用__stdcall 调用约定。据我所知,这完全消除了 lambdas 作为一个选项,否则有点麻烦。我也想至少允许__cdecl。
这是我能想到的最好的方法,而不会开始转向依赖函数指针所没有的支持。它通常在标题中。以下是Coliru 上的示例。
#include <functional>
//C function in another header I have no control over
extern "C" void foo(int(__stdcall *callback)()) {
callback();
}
namespace detail {
std::function<int()> callback; //pretend extern and defined in cpp
//compatible with the API, but passes work to above variable
extern "C" int __stdcall proxyCallback() { //pretend defined in cpp
//possible additional processing
return callback();
}
}
template<typename F> //takes anything
void wrappedFoo(F f) {
detail::callback = f;
foo(detail::proxyCallback); //call C function with proxy
}
int main() {
wrappedFoo([&]() -> int {
return 5;
});
}
但是,有一个重大缺陷。这不是重入。如果变量在使用之前被重新分配,则永远不会调用旧函数(不考虑多线程问题)。
我尝试过的一件事最终导致自身加倍,是将std::function 存储为数据成员并使用对象,因此每个对象都会对不同的变量进行操作,但无法将对象传递给代理.将对象作为参数会导致签名不匹配并且绑定它不会让结果存储为函数指针。
我有一个想法,但没有尝试过是std::function 的向量。但是,我认为唯一真正安全的擦除它的时间是在没有任何东西使用它时清除它。但是,每个条目首先添加到wrappedFoo,然后在proxyCallback 中使用。我想知道在前者中递增并在后者中递减,然后在清除向量之前检查为零的计数器是否可以工作,但这听起来像是一个比必要的更复杂的解决方案。
有没有办法用函数指针回调包装 C 函数,使得 C++ 包装版本:
- 允许任何函数对象
- 不仅允许 C 回调的调用约定(如果重要的是它必须相同,用户可以通过正确的调用约定传入某些内容)
- 线程安全/可重入
注意:作为 Mikael Persson 回答的一部分,显而易见的解决方案是使用应该存在的 void * 参数。然而,遗憾的是,这不是一个万能的、最终的选择,主要是由于无能。对于那些没有此选项的函数存在哪些可能性,这可能会变得有趣,并且是获得非常有用的答案的主要途径。
【问题讨论】:
-
"这不是可重入的。如果变量在使用之前被重新分配,则永远不会调用旧函数(不考虑多线程问题)。"如果发生这种情况,那么您不是正在从两个不同的线程设置相同的回调吗?除非您注册的回调系统可以处理这个问题,否则您的包装系统是否可以处理并不重要。 Broken 被破坏了,无论是在你的代码中还是在他们的代码中。
-
@NicolBolas,嗯,绝对可以。我不愿提及这个问题是从 Windows API
CreateWindow产生的,它应该可以同时从两个线程调用 AFAIK,并且还有一个(更隐藏的)void *选项。犹豫的原因是我的意思是这个问题比这更抽象一点。不过,这是一个值得考虑的好点。 -
@NicolBolas 所描述的情况是当同一个包装函数(回调指针指向的)用于注册多个回调(在多次调用 C API 函数之后),如果您只有一个全局函数对象(指向实际的回调函子 / lambda / 其他)。这与多线程本身无关,尽管多线程确实引入了更多问题。
-
@MikaelPersson,我对自己感到震惊,但出于某种原因,我什至没有想到这一点。这真的很重要。
-
@chris:您的问题应该只适用于不采用用户定义数据参数的回调(即:CreateWindow 采用的
lpParam)。任何具有此类存储的回调都可以。唯一需要诉诸全局恶作剧的是那些不采用此类参数的真正烦人的回调。
标签: c++ c++11 lambda callback stdbind