在没有上下文参数的情况下将有状态 lambda 传递给 C 样式函数

Passing stateful lambda into C style function without context-argument

我正在尝试将 C 库集成到我的 C++ 项目中。 C 库具有将函数指针作为参数的函数,但这些函数指针被写为 typedef。

typedef void(*FileHandler_t)(File* handle);

然后像这样注册回调的函数:

void RegisterCallback(FileHandler_t handler);

我可以创建一个 lambda 表达式并将其传递到参数处理程序的 RegisterCallback 中

auto handler = [](File* handle){ //handle cb };

这很好用。

RegisterCallback(handler);

但是当我尝试传递要在处理程序内部使用的局部变量时,我收到编译器错误。

auto handler = [&container](File* handle){ //handle cb };

现在 RegisterCallback 不再编译。 我想这样做是因为我不想使用全局变量。在这些场景中正式使用的错误是否存在?

据我所知,除了修改库本身之外别无他法。

Lambdas 通常有一个你不能依赖的类型;只有在他们没有捕获任何东西的情况下,类型“decay to”(等于)才是函数指针。

最常见的解决方法是在 lambda 中使用静态数据:

#include <iostream>
#include <vector>

typedef void(*FuncInt)(int);

static bool DataTheFunctionNeeds = true;

int main() {
    FuncInt a = [](int) { };
    FuncInt b = [](int) { if(DataTheFunctionNeeds){} };
}

通常,像这样使用回调的 C 库的设计者会提供一些方法将状态变量关联到回调。通常是 void* 参数。某些库在您传递回调时采用该状态指针,并将指针传递给回调函数,例如WinAPI 通常这样做,参见 EnumWindows。其他库允许将那个东西放入它们传递给回调的某个对象中,例如libpng 使用 png_set_write_fnpng_get_io_ptr API 函数来做到这一点。

如果在阅读可用文档后您得出结论,您的图书馆并非如此,并且您不能或不想向图书馆作者寻求支持,这意味着您必须更有创意。

一种解决方法是使用散列映射将文件与容器相关联。像这样:

static std::unordered_map<File*, Container*> s_containers;

RegisterCallback之前将文件与容器相关联,并在回调中通过文件指针查找容器。考虑线程,也许您需要使用互斥锁来保护静态哈希映射。还要考虑异常处理,也许你需要RAII class在析构函数中的constructor/unregister中注册。

另一种更简单但更受限制的方法是使用 C++/11 中引入的 thread_local 存储 class 说明符,declare

static thread_local Container* s_container;

并在回调中使用它。如果你的库确实阻塞 IO 并且不在内部使用线程,那么这很有可能会正常工作。但是您仍然需要处理错误,即当容器超出范围时将全局变量重置为 nullptr

更新: 如果您可以更改库,这样做比这两种解决方法都要好得多。将另一个 void* 参数传递给 RegisterCallback,并将处理程序更改为 typedef void(*FileHandler_t)(File* handle, void* context); 如果您从 C++ 使用库,通常最好将回调实现为私有静态方法,并传递 this 指向库的指针。这将允许您在回调中调用实例方法,同时保持 class 隐藏的内部状态。

如您所说,回调函数中没有void*指针,通常用于识别。

但是,作为解决方法,您可以引入一个 class 模板来为您存储此信息。 live demo:

template<typename FN>
class dispatcher {
    static inline std::function<void(int)> fn_;

    static void foo(int x) { fn_(x); }

public:
    static fn_type dispatch(FN fn) {
        fn_ = fn;
        return &dispatcher::foo;
    }
};

class是为了解决成员函数fn_.

二义性的模板

不过,这对 classes 来说效果不是很好。但是您可以更改它,以便提供某种自己的 "disambiguiteer"。 live demo:

template<int DSP>
class dispatcher {
    static inline std::function<void(int)> fn_;

    static void foo(int x) { fn_(x); }

public:
    template<typename FN>
    static fn_type dispatch(FN fn) {
        fn_ = fn;
        return &dispatcher::foo;
    }
};

因为 lambda 有状态,所以将这样的 lambda 转换为函数指针将涉及将 lambda 状态存储在具有静态存储持续时间的变量中。

这显然容易出错,因为在每次转换为函数指针时,静态存储持续时间状态都会被修改,从而导致很难跟踪错误。这当然是lambda with state不能转换为函数指针的原因。

另一方面,如果你明白什么是危险,你可以实现静态存储持续时间lambda转换。 (必须确保在转换为函数指针后,不会访问之前转换的 lambda 实例):

#include <utility>
#include <new>

//bellow, a more efficient version of std::function
//for holding (statically) typed lambda of static storage duration
//Because lamda type depends on the function in which they are defined
//this is much more safer than a naive implementation using std::function.
template<class LT>
struct static_lambda{
  private:
    static inline unsigned char buff alignas(LT) [sizeof(LT)];
    static inline LT* p=nullptr;
    static inline struct raii_guard{
      ~raii_guard(){
         if (p) p->~LT();
         }
      } guard{};
  public:
    static_lambda(LT g_){
       if (p) p->~LT();
       p=new (buff) LT{std::move(g_)};
       }

    static_lambda(const static_lambda&)=delete //for safety

    template<class...Args>
    static auto execute(Args...args){
      return (*p)(std::forward<Args>(args)...);
      }
    template<class FT>
    operator FT*() && {//by rvalue reference for safety reason
      return execute;
      }
  };


using File = int;
typedef void(*FileHandler_t)(File* handle);
void RegisterCallback(FileHandler_t handler);

using container_type=double*;

void test(container_type& container){
  //each time this function is called
  //the previously registered lambda is destroyed
  //and should not be accessed (which is certainly the case).
  RegisterCallback(static_lambda{[&container](File* h){}});
  }

Demo