С ++ лямбда з захопленнями як покажчик функції


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?
Ramon Zarazua B.

версія gcc 4.6.1 20110801 [редакція gcc-4_6-гілки 177033] (SUSE Linux)
duncan

4
Зазвичай спосіб передачі стану C до зворотних викликів здійснюється за допомогою додаткового аргументу до зворотного виклику (зазвичай типу void *). Якщо бібліотека, яку ви використовуєте, дозволяє використовувати цей додатковий аргумент, ви знайдете обхідний шлях. В іншому випадку у вас немає можливості досягти чисто того, що ви хочете зробити.
Alexandre C.

Так. Я розумію, що API ftw.h та nftw.h є недосконалим. Я спробую fts.h
duncan

1
Чудово! /usr/include/fts.h:41:3: помилка: # помилка "<fts.h> не може використовуватися з -D_FILE_OFFSET_BITS == 64"
duncan

Відповіді:


48

Оскільки для захоплення лямбд потрібно зберегти стан, насправді не існує простого "обхідного шляху", оскільки це не просто звичайні функції. Сенс щодо покажчика функції полягає в тому, що він вказує на єдину глобальну функцію, і ця інформація не має місця для стану.

Найближчим обхідним шляхом (який по суті відкидає стан) є надання певного типу глобальної змінної, до якої можна отримати доступ з вашої лямбда-функції. Наприклад, ви можете створити традиційний функторний об'єкт і надати йому функцію статичного члена, яка посилається на якийсь унікальний (глобальний / статичний) екземпляр.

Але це начебто розгромило всю мету захоплення лямбд.


3
Чистішим рішенням є загортання лямбда-сигналу всередину адаптера, припускаючи, що покажчик функції має параметр контексту.
Raymond Chen

4
@RaymondChen: Ну, якщо ви вільні визначити, як використовувати цю функцію, то так, це варіант. Хоча в такому випадку було б ще простіше просто зробити параметр аргументом самої лямбди!
Kerrek SB

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

"покажчик функції вказує на єдину глобальну функцію, і ця інформація не має місця для стану." -> Як, на біса, такі мови, як Java, можуть це зробити? Ну, звичайно, адже ця єдина глобальна функція створюється під час виконання та вбудовує стан (вірніше, посилання на нього) у власний код. Те є вся суть - повинна НЕ бути єдиної, глобальна функцією , але кілька глобальних функцій - один для кожної тимчасової лямбда використовуються під час виконання. Невже в С ++ немає НІЧОГО, що це робить? (Я думав, що функція std :: створена саме для цієї єдиної цілі)
Декстер,

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;
}

72
Ні, це не повинно бути прийнятою відповіддю. Справа не змінюється ftwвзяти std::functionзамість покажчика на функцію ...
Григорій Pakosz

Друге рішення, запропоноване в цій відповіді, стосується занепокоєння @ gregory-pakosz, зберігаючи оригінальний підпис, але воно все ще не є чудовим, оскільки воно вводить глобальний стан. Якби ftwаргумент void * userdata містив, то я б віддав перевагу відповіді від @ evgeny-karpov.
prideout

@prideout погодився - мені теж не подобається глобальна держава. На жаль, припускаючи, що підпис ftw не може бути змінений, і зважаючи на те, що він не має порожніх * userdata, стан має десь зберігатися. Я зіткнувся з цією проблемою, використовуючи сторонні бібліотеки. Це буде працювати нормально, доки бібліотека не захопить зворотний виклик і не використає його пізніше, і в цьому випадку глобальна змінна просто діє як додатковий параметр у стеку викликів. Якщо підпис ftw можна змінити, я віддаю перевагу використанню функції std :: замість void * userdata.
Jay West

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 ++ із захопленнями як покажчиком функції. Оскільки це було корисно для мене та інших людей, я дещо покращив.

Стандартна функція C вказівника api використовує умову 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;
    }
};

Приклад

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, щоб вона могла передати його покажчику функції під час його виклику.
Кодоскоп

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) + аргументи (при виклику). В іншому випадку це елегантне рішення
Іван Санц-Караса

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

1
@ IvanSanz-Carasa Дякую за вказівку. Типи закриття не є CopyAssnanable, але функтори. Тож правильно, тут краще використовувати ідеальну пересилання. Для аргументів, з іншого боку, ми не можемо багато чого зробити, оскільки звичайний 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();
}

-1

Знайшов відповідь тут: http://meh.schizofreni.co/programming/magic/2013/01/23/function-pointer-from-lambda.html

Він перетворює lambda pointerв void*і звернений назад , коли це необхідно.

  1. до void*:

    auto voidfunction = новий тип declt (до_функції (лямбда)) (до_функції (лямбда));

  2. від void*:

    автоматична функція = static_cast <std :: function *> (voidfunction);

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.