Передача лямбда с сохранением состояния в функцию стиля C без аргумента контекста - PullRequest
0 голосов
/ 12 октября 2018

Я пытаюсь интегрировать библиотеку C в мой проект C ++.В библиотеке C есть функции, которые принимают указатели функций в качестве аргументов, но эти указатели функций записываются как typedefs.

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

Затем функция для регистрации обратного вызова выглядит так:

void RegisterCallback(FileHandler_t handler);

Я могусоздайте лямбда-выражение и передайте его в RegisterCallback для обработчика аргумента

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

И это прекрасно работает.

RegisterCallback(handler);

Но когда я пытаюсь передать локальные переменные для использования внутри обработчикаЯ получаю ошибки компилятора.

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

Теперь RegisterCallback больше не компилируется.Я хотел сделать это таким образом, потому что я не хотел использовать глобальные переменные.Есть ли в какой-то мере ошибка, формально используемая в этих сценариях?

Из того, что я вижу, нет другого пути, кроме как изменить саму библиотеку.

Ответы [ 4 ]

0 голосов
/ 13 октября 2018

Поскольку у лямбды есть состояние, преобразование такой лямбды в указатель на функцию будет включать в себя сохранение лямбда-состояния в переменных со статической продолжительностью хранения.

Это, очевидно, подвержено ошибкам, поскольку при каждом преобразовании в функциюуказатель, состояние длительности статической памяти будет изменено, что приведет к очень сложным для отслеживания ошибок.Это, безусловно, причина, по которой лямбда с состоянием не может быть преобразована в указатель на функцию.

С другой стороны, если вы понимаете, в чем опасность, вы можете реализовать статическое лямбда-преобразование продолжительности хранения.(вы должны быть уверены, что после преобразования в указатель на функцию ранее преобразованный лямбда-экземпляр не будет доступен):

#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

0 голосов
/ 12 октября 2018

Обычно разработчики библиотек C, которые используют такие обратные вызовы, предоставляют некоторый способ связать переменную состояния с обратным вызовом.Часто аргумент void*.Некоторые библиотеки принимают этот указатель состояния при передаче обратного вызова и передают указатель в функцию обратного вызова, например, обычно это делает WinAPI, см. EnumWindows .Другие библиотеки позволяют помещать эту вещь в некоторый объект, который они передают в обратный вызов, например, libpng делает это с API-функциями png_set_write_fn и png_get_io_ptr.

Если после прочтения доступной документации вы пришли к выводу, что это не такдля вашей библиотеки, и вы не можете или не хотите обращаться за поддержкой к автору библиотеки, это означает, что вам нужно быть более креативным.

Один из обходных путей - использование хэш-карты для связи файлов с контейнерами.Например:

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

Свяжите файл с контейнером до RegisterCallback, и при обратном вызове ищите указатель файла, чтобы найти контейнер.Подумайте о потоке, возможно, вам нужно защитить статическую хеш-карту с помощью мьютекса.Также подумайте об обработке исключений, возможно, вам нужен класс RAII для регистрации в конструкторе / отмене регистрации в деструкторе.

Другой, гораздо более простой, но более ограниченный метод, использует спецификатор класса памяти thread_local, представленный в C ++ / 11, объявить

static thread_local Container* s_container;

И использовать его в обратном вызове.Если ваша библиотека блокирует IO и внутренне не использует многопоточность, есть хороший шанс, что это будет работать нормально.Но все же вам нужно обрабатывать ошибки, то есть сбрасывать глобальную переменную на nullptr, когда контейнер выходит из области видимости.

Обновление: Если вы можете изменить библиотеку, сделать это намного лучшечем оба обходных пути.Передайте еще один аргумент void* в RegisterCallback и измените обработчик на typedef void(*FileHandler_t)(File* handle, void* context); Если вы используете библиотеку из C ++, обычно рекомендуется реализовать обратный вызов как частный статический метод и передать указатель this в библиотеку.Это позволит вам вызывать методы экземпляра в обратном вызове, сохраняя при этом внутреннее состояние вашего класса скрытым.

0 голосов
/ 12 октября 2018

Как вы упомянули, в функции обратного вызова нет указателя 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_.

Это не очень хорошо работаетхорошо с классами, хотя.Но вы можете изменить это так, чтобы вы предоставили своего рода «двусмысленность». живое демо :

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;
    }
};
0 голосов
/ 12 октября 2018

Лямбды обычно имеют тип, на который нельзя положиться;только в том случае, если они ничего не фиксируют, тип " decay to " (равен) указателю на функцию.

Наиболее распространенный обходной путь - использование статических данных в лямбда-выражениивместо:

#include <iostream>
#include <vector>

typedef void(*FuncInt)(int);

static bool DataTheFunctionNeeds = true;

int main() {
    FuncInt a = [](int) { };
    FuncInt b = [](int) { if(DataTheFunctionNeeds){} };
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...