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

时间:2018-10-12 11:30:52

标签: c++ lambda function-pointers

我正在尝试将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不再编译。 我想这样做,因为我不想使用全局变量。反过来在这些情况下会正式使用该错误吗?

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

4 个答案:

答案 0 :(得分:4)

通常,使用此类回调的C库设计​​人员提供了某种将状态变量与回调关联的方法。通常为void*参数。某些库在传递回调时采用该状态指针,然后将指针传递给回调函数,例如WinAPI通常会这样做,请参见EnumWindows。其他库允许将该内容放入它们传递给回调的某个对象中,例如libpng使用png_set_write_fnpng_get_io_ptr API函数来做到这一点。

如果在阅读了可用的文档后得出结论,认为图书馆不是这种情况,并且您不能或不想向图书馆作者寻求支持,则意味着您必须更具创造力。

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

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

RegisterCallback之前将文件与容器相关联,并在回调中通过文件指针查找以找出容器。考虑线程问题,也许您需要使用互斥量来保护该静态哈希映射。再考虑一下异常处理,也许您需要RAII类在构造函数中注册/在析构函数中取消注册。

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

static thread_local Container* s_container;

并在回调中使用它。如果您的媒体库确实阻止了IO,并且未在内部使用线程,则很有可能可以正常运行。但是仍然需要处理错误,即当容器超出范围时将全局变量重置为nullptr

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

答案 1 :(得分:0)

Lambda通常具有您不能依赖的类型;仅在它们什么都没捕获的情况下,类型“ Gantry”(等于)才是函数指针。

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

#include <iostream>
#include <vector>

typedef void(*FuncInt)(int);

static bool DataTheFunctionNeeds = true;

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

答案 2 :(得分:0)

如前所述,在回调函数中通常没有用于标识的void*指针。

但是,作为一种解决方法,您可以引入一个类模板来为您存储此信息。 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;
    }
};

该类是用于解决成员函数fn_的歧义的模板。

但是,这在类上不能很好地工作。但是您可以更改它,以便提供某种自己的“歧义消除器”。 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;
    }
};

答案 3 :(得分:0)

由于lambda具有状态,因此将此类lambda转换为函数指针将涉及将lambda状态存储在具有静态存储持续时间的变量中。

这显然很容易出错,因为在每次转换为函数指针时,都会修改静态存储持续时间状态,从而导致很难跟踪错误。这肯定是为什么带状态的lambda无法转换为函数指针的原因。

反之,如果您了解有什么危险,则可以实现静态存储时间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