Лямбда C ++ с захватами в качестве указателя на функцию


94

Я играл с лямбдами C ++ и их неявным преобразованием в указатели на функции. В моем начальном примере они использовались в качестве обратного вызова для функции ftw. Это работает, как ожидалось.

#include <ftw.h>
#include <iostream>

using namespace std;

int main()
{
    auto callback = [](const char *fpath, const struct stat *sb,
        int typeflag) -> int {
        cout << fpath << endl;
        return 0;
    };

    int ret = ftw("/etc", callback, 1);

    return ret;
}

После модификации для использования захватов:

int main()
{

    vector<string> entries;

    auto callback = [&](const char *fpath, const struct stat *sb,
        int typeflag) -> int {
        entries.push_back(fpath);
        return 0;
    };

    int ret = ftw("/etc", callback, 1);

    for (auto entry : entries ) {
        cout << entry << endl;
    }

    return ret;
}

У меня ошибка компилятора:

error: cannot convert ‘main()::<lambda(const char*, const stat*, int)>’ to ‘__ftw_func_t {aka int (*)(const char*, const stat*, int)}’ for argument ‘2’ to ‘int ftw(const char*, __ftw_func_t, int)

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

Есть ли обходной путь для этого? Означает ли тот факт, что они не могут быть преобразованы «неявно», что они могут быть преобразованы «явно»? (Я пробовал кастинг, но безуспешно). Каким будет чистый способ изменить рабочий пример, чтобы я мог добавлять записи к какому-либо объекту с помощью лямбда-выражений?


Какой компилятор вы используете? это VS10?
Рамон Заразуа Б.

gcc версия 4.6.1 20110801 [gcc-4_6-ветка версия 177033] (SUSE Linux)
Дункан

4
Обычно способ передачи состояния в обратные вызовы в языке C осуществляется с помощью дополнительного аргумента обратного вызова (обычно типа void *). Если используемая вами библиотека допускает этот дополнительный аргумент, вы найдете обходной путь. В противном случае у вас не будет возможности чисто достичь того, что вы хотите делать.
Alexandre C.

Да. Я понимаю, что api ftw.h и nftw.h ошибочны. Я попробую fts.h
Дункан

1
Большой! /usr/include/fts.h:41:3: error: #error "<fts.h> не может использоваться с -D_FILE_OFFSET_BITS == 64"
Дункан

Ответы:


48

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

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

Но это своего рода поражение всей цели захвата лямбд.


3
Более чистое решение - обернуть лямбду внутри адаптера, предполагая, что указатель функции имеет параметр контекста.
Raymond Chen

4
@RaymondChen: Что ж, если вы вольны определять, как использовать функцию, то да, это вариант. Хотя в этом случае было бы еще проще сделать параметр аргументом самой лямбды!
Kerrek SB

3
@KerrekSB поместил глобальные переменные в a namespaceи пометил их как thread_local, это ftwподход, который я выбрал для решения чего-то подобного.
Kjell Hedström

«указатель функции указывает на единственную глобальную функцию, и в этой информации нет места для состояния». -> Как же, черт возьми, языки вроде Java могут это сделать? Ну, конечно, потому что эта единственная глобальная функция создается во время выполнения и встраивает состояние (или, скорее, ссылку на него) в свой собственный код. То есть вся суть - должна не быть единой, глобальная функцией , но несколько глобальных функций - один для каждой временной лямбды используются во время выполнения. Неужели в C ++ НИЧЕГО нет такого? (Я думал, что std :: function создана именно для этой единственной цели)
Декстер

1
@Dexter: эээ ... короткий ответ - нет, длинный ответ связан с перегрузкой оператора. Тем не менее, моя точка зрения остается неизменной. Java - это другой язык, который отличается от C ++; В Java нет указателей (или перегружаемых операторов вызова), и сравнение не работает.
Керрек С.Б.

47

Я просто столкнулся с этой проблемой.

Код отлично компилируется без лямбда-захвата, но есть ошибка преобразования типа с лямбда-захватом.

Решение на C ++ 11 - использовать std::function(изменить: после этого примера показано другое решение, не требующее изменения сигнатуры функции). Вы также можете использовать boost::function(который на самом деле работает значительно быстрее). Пример кода - изменен так, чтобы он компилировался, скомпилирован с помощью gcc 4.7.1:

#include <iostream>
#include <vector>
#include <functional>

using namespace std;

int ftw(const char *fpath, std::function<int (const char *path)> callback) {
  return callback(fpath);
}

int main()
{
  vector<string> entries;

  std::function<int (const char *fpath)> callback = [&](const char *fpath) -> int {
    entries.push_back(fpath);
    return 0;
  };

  int ret = ftw("/etc", callback);

  for (auto entry : entries ) {
    cout << entry << endl;
  }

  return ret;
}

Изменить: мне пришлось вернуться к этому, когда я столкнулся с устаревшим кодом, в котором я не мог изменить исходную сигнатуру функции, но все же нужно было использовать лямбды. Решение, которое не требует изменения сигнатуры исходной функции, приведено ниже:

#include <iostream>
#include <vector>
#include <functional>

using namespace std;

// Original ftw function taking raw function pointer that cannot be modified
int ftw(const char *fpath, int(*callback)(const char *path)) {
  return callback(fpath);
}

static std::function<int(const char*path)> ftw_callback_function;

static int ftw_callback_helper(const char *path) {
  return ftw_callback_function(path);
}

// ftw overload accepting lambda function
static int ftw(const char *fpath, std::function<int(const char *path)> callback) {
  ftw_callback_function = callback;
  return ftw(fpath, ftw_callback_helper);
}

int main() {
  vector<string> entries;

  std::function<int (const char *fpath)> callback = [&](const char *fpath) -> int {
    entries.push_back(fpath);
    return 0;
  };
  int ret = ftw("/etc", callback);

  for (auto entry : entries ) {
    cout << entry << endl;
  }

  return ret;
}

73
Нет, это не должен быть принятый ответ. Дело не в том, ftwчтобы взять std::functionвместо указателя функции ...
Грегори Пакош

Второе решение, предложенное в этом ответе, решает проблему @ gregory-pakosz, сохраняя исходную подпись, но это все еще не очень хорошо, потому что оно вводит глобальное состояние. Если ftwбы был аргумент void * userdata, я бы предпочел ответ от @ evgeny-karpov.
prideout

@prideout согласился - мне тоже не нравится глобальное состояние. К сожалению, если предположить, что подпись ftw не может быть изменена, и учитывая, что он не имеет void * userdata, состояние должно где-то храниться. Я столкнулся с этой проблемой, используя стороннюю библиотеку. Это будет работать нормально, пока библиотека не захватит обратный вызов и не использует его позже, и в этом случае глобальная переменная просто действует как дополнительный параметр в стеке вызовов. Если подпись ftw может быть изменена, я бы предпочел использовать std :: function вместо void * userdata.
Джей Уэст

1
это чрезвычайно сложное и полезное решение, @Gregory, я должен вам сказать, что «оно работает».
fiorentinoing

17

ОРИГИНАЛ

Лямбда-функции очень удобны и сокращают код. В моем случае мне понадобились лямбды для параллельного программирования. Но для этого требуется захват и указатели функций. Мое решение здесь. Но будьте осторожны с набором переменных, которые вы захватили.

template<typename Tret, typename T>
Tret lambda_ptr_exec(T* v) {
    return (Tret) (*v)();
}

template<typename Tret = void, typename Tfp = Tret(*)(void*), typename T>
Tfp lambda_ptr(T& v) {
    return (Tfp) lambda_ptr_exec<Tret, T>;
}

пример

int a = 100;
auto b = [&]() { a += 1;};
void (*fp)(void*) = lambda_ptr(b);
fp(&b);

Пример с возвращаемым значением

int a = 100;
auto b = [&]() {return a;};
int (*fp)(void*) = lambda_ptr<int>(b);
fp(&b);

ОБНОВИТЬ

Улучшенная версия

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

API указателя стандартной функции C использует соглашение void fn (void * data). По умолчанию используется это соглашение, и лямбда должна быть объявлена ​​с аргументом void *.

Улучшенная реализация

struct Lambda {
    template<typename Tret, typename T>
    static Tret lambda_ptr_exec(void* data) {
        return (Tret) (*(T*)fn<T>())(data);
    }

    template<typename Tret = void, typename Tfp = Tret(*)(void*), typename T>
    static Tfp ptr(T& t) {
        fn<T>(&t);
        return (Tfp) lambda_ptr_exec<Tret, T>;
    }

    template<typename T>
    static void* fn(void* new_fn = nullptr) {
        static void* fn;
        if (new_fn != nullptr)
            fn = new_fn;
        return fn;
    }
};

Exapmle

int a = 100;
auto b = [&](void*) {return ++a;};

Преобразование лямбда с захватами в указатель C

void (*f1)(void*) = Lambda::ptr(b);
f1(nullptr);
printf("%d\n", a);  // 101 

Можно использовать и так

auto f2 = Lambda::ptr(b);
f2(nullptr);
printf("%d\n", a); // 102

Если нужно использовать возвращаемое значение

int (*f3)(void*) = Lambda::ptr<int>(b);
printf("%d\n", f3(nullptr)); // 103

И в случае использования данных

auto b2 = [&](void* data) {return *(int*)(data) + a;};
int (*f4)(void*) = Lambda::ptr<int>(b2);
int data = 5;
printf("%d\n", f4(&data)); // 108

3
Это определенно самое удобное решение, которое я видел для преобразования лямбда-выражения в указатель на функцию в стиле C. Функция, принимающая его в качестве аргумента, будет нуждаться только в дополнительном параметре, представляющем ее состояние, часто называемом «void * user» в библиотеках C, чтобы она могла передать его указателю функции при ее вызове.
Codoscope

10

Используя локально глобальный (статический) метод, это можно сделать следующим образом

template <class F>
auto cify_no_args(F&& f) {
  static F fn = std::forward<F>(f);
  return [] {
    return fn();
  };
}

Предположим, у нас есть

void some_c_func(void (*callback)());

Таким образом, использование будет

some_c_func(cify_no_args([&] {
  // code
}));

Это работает, потому что каждая лямбда имеет уникальную подпись, поэтому сделать ее статической не проблема. Ниже приведена общая оболочка с переменным числом аргументов и любым типом возвращаемого значения, использующим тот же метод.

template <class F>
struct lambda_traits : lambda_traits<decltype(&F::operator())>
{ };

template <typename F, typename R, typename... Args>
struct lambda_traits<R(F::*)(Args...)> : lambda_traits<R(F::*)(Args...) const>
{ };

template <class F, class R, class... Args>
struct lambda_traits<R(F::*)(Args...) const> {
    using pointer = typename std::add_pointer<R(Args...)>::type;

    static pointer cify(F&& f) {
        static F fn = std::forward<F>(f);
        return [](Args... args) {
            return fn(std::forward<Args>(args)...);
        };
    }
};

template <class F>
inline lambda_traits<F>::pointer cify(F&& f) {
    return lambda_traits<F>::cify(std::forward<F>(f));
}

И подобное использование

void some_c_func(int (*callback)(some_struct*, float));

some_c_func(cify([&](some_struct* s, float f) {
    // making use of "s" and "f"
    return 0;
}));

1
имейте в виду, что это скопирует закрытие (при получении ptr) + args (при вызове). В противном случае это элегантное решение
Иван Санс-Караса

вспомогательная библиотека только для заголовков: gist.github.com/isc30/fab67e5956fe8f2097bed84ebc42c1e8
Иван Санс-Караса

1
@ IvanSanz-Carasa Спасибо, что указали. Типы замыкания - это не CopyAssignable, а функторы. Итак, вы правы, здесь лучше использовать идеальную переадресацию. С другой стороны, для аргументов мы мало что можем сделать, поскольку простой C не поддерживает универсальные ссылки, но, по крайней мере, мы можем пересылать значения обратно в нашу лямбду. Это может сэкономить дополнительную копию. Отредактировал код.
Владимир Талыбин

@RiaD Да, потому что лямбда здесь статический экземпляр, вам нужно будет захватить по ссылке, например, вместо =использования &iв вашем цикле for.
Владимир Талыбин

5

Хе-хе - довольно старый вопрос, но все же ...

#include <iostream>
#include <vector>
#include <functional>

using namespace std;

// We dont try to outsmart the compiler...
template<typename T>
int ftw(const char *fpath, T callback) {
  return callback(fpath);
}

int main()
{
  vector<string> entries;

  // ... now the @ftw can accept lambda
  int ret = ftw("/etc", [&](const char *fpath) -> int {
    entries.push_back(fpath);
    return 0;
  });

  // ... and function object too 
  struct _ {
    static int lambda(vector<string>& entries, const char* fpath) {
      entries.push_back(fpath);
      return 0;
    }
  };
  ret = ftw("/tmp", bind(_::lambda, ref(entries), placeholders::_1));

  for (auto entry : entries ) {
    cout << entry << endl;
  }

  return ret;
}

0

Есть хакерский способ преобразовать захватывающую лямбду в указатель на функцию, но вы должны быть осторожны при его использовании:

/codereview/79612/c-ifying-a-capturing-lambda

Тогда ваш код будет выглядеть так (предупреждение: компиляция мозга):

int main()
{

    vector<string> entries;

    auto const callback = cify<int(*)(const char *, const struct stat*,
        int)>([&](const char *fpath, const struct stat *sb,
        int typeflag) -> int {
        entries.push_back(fpath);
        return 0;
    });

    int ret = ftw("/etc", callback, 1);

    for (auto entry : entries ) {
        cout << entry << endl;
    }

    return ret;
}

0

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

typedef int (* MYPROC)(int);

void fun(MYPROC m)
{
    cout << m(100) << endl;
}

template<class T>
void fun2(T f)
{
    cout << f(100) << endl;
}

void useLambdaAsFunPtr()
{
    int p = 7;
    auto f = [p](int a)->int {return a * p; };

    //fun(f);//error
    fun2(f);
}

void useLambdaAsFunPtr2()
{
    int p = 7;
    static auto f = [p](int a)->int {return a * p; };
    MYPROC ff = [](int i)->int { return f(i); };
    //here, it works!
    fun(ff);
}

void test()
{
    useLambdaAsFunPtr2();
}

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.