Проходження захоплення лямбда як вказівник функції


210

Чи можливо передавати лямбда-функцію як функцію вказівника? Якщо так, я повинен робити щось неправильно, тому що я отримую помилку компіляції.

Розглянемо наступний приклад

using DecisionFn = bool(*)();

class Decide
{
public:
    Decide(DecisionFn dec) : _dec{dec} {}
private:
    DecisionFn _dec;
};

int main()
{
    int x = 5;
    Decide greaterThanThree{ [x](){ return x > 3; } };
    return 0;
}

Коли я намагаюся скласти це , я отримую таку помилку компіляції:

In function 'int main()':
17:31: error: the value of 'x' is not usable in a constant expression
16:9:  note: 'int x' is not const
17:53: error: no matching function for call to 'Decide::Decide(<brace-enclosed initializer list>)'
17:53: note: candidates are:
9:5:   note: Decide::Decide(DecisionFn)
9:5:   note: no known conversion for argument 1 from 'main()::<lambda()>' to 'DecisionFn {aka bool (*)()}'
6:7:   note: constexpr Decide::Decide(const Decide&)
6:7:   note: no known conversion for argument 1 from 'main()::<lambda()>' to 'const Decide&'
6:7:   note: constexpr Decide::Decide(Decide&&)
6:7:   note: no known conversion for argument 1 from 'main()::<lambda()>' to 'Decide&&'

Це один пробіл повідомлення про помилку, який можна засвоїти, але я думаю, що я виходжу з нього, це те, що лямбда не може трактуватися як constexprтак, тому я не можу передавати її як покажчик функції? Я також спробував зробити xconst, але це, здається, не допоможе.


34
лямбда може занепадати, щоб функціонувати вказівник, лише якщо вони нічого не захоплюють.
Jarod42


Щодо нащадків, публікація вищезазначеного блогу зараз живе за адресою devblogs.microsoft.com/oldnewthing/20150220-00/?p=44623
warrenm

Відповіді:


205

Лямбда може бути перетворена на покажчик функції, лише якщо вона не захоплює, із чернетки стандартного розділу C ++ 11 5.1.2 [expr.prim.lambda] сказано ( моє наголос ):

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

Зауважте, cppreference також висвітлює це у своєму розділі про функції лямбда .

Отже, наступні альтернативи спрацюють:

typedef bool(*DecisionFn)(int);

Decide greaterThanThree{ []( int x ){ return x > 3; } };

і так би це:

typedef bool(*DecisionFn)();

Decide greaterThanThree{ [](){ return true ; } };

і як зазначає 5gon12eder , ви також можете користуватися std::function, але зауважте, що std::functionце велика вага , тому це не дешева торгівля.


2
Бічна примітка: Одне поширене рішення, яке використовується C-речами, - це передавання а void*як єдиного параметра. Зазвичай його називають "покажчиком користувача". Він також порівняно легкий, але, як правило, вимагає mallocвиділити трохи місця.
Фонд позову Моніки

94

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

  1. Використовуйте std::functionзамість необроблених покажчиків функцій.

    Це дуже чисте рішення. Однак зауважте, що вона включає деякі додаткові накладні витрати для стирання типу (можливо, виклик віртуальної функції).

    #include <functional>
    #include <utility>
    
    struct Decide
    {
      using DecisionFn = std::function<bool()>;
      Decide(DecisionFn dec) : dec_ {std::move(dec)} {}
      DecisionFn dec_;
    };
    
    int
    main()
    {
      int x = 5;
      Decide greaterThanThree { [x](){ return x > 3; } };
    }
  2. Використовуйте лямбда-вираз, який нічого не фіксує.

    Оскільки ваш предикат - це лише булева константа, наступне питання швидко обійдеться. Дивіться цю відповідь, щоб добре пояснити, чому і як це працює.

    // Your 'Decide' class as in your post.
    
    int
    main()
    {
      int x = 5;
      Decide greaterThanThree {
        (x > 3) ? [](){ return true; } : [](){ return false; }
      };
    }

4
@TC Перегляньте це питання для детальної інформації, чому воно працює
Шафік Ягмур

Зауважте, що загалом, якщо ви знаєте дані захоплення під час компіляції, ви можете перетворити їх у тип даних, а потім повернетесь до того, що лямбда не має захоплення - дивіться цю відповідь, яку я щойно написав на інше запитання (дякую @ Тут відповідь 5gon12eder).
дан-людина

Чи не повинен об’єкт мати довший термін служби, ніж функція вказівника? Я хотів би використовувати його для glutReshapeFunc.
ar2015

я не рекомендую цю пропозицію: речі, які, як правило, працюють магічно, вводять нові помилки. і практики, які поєднуються з тими помилками. якщо ви хочете використовувати std :: function, ви повинні побачити всі види способів використання std :: function. тому що деякі способи, можливо, те, чого ти не хочеш.
TheNegative

1
Це не відповідає на запитання. Якщо хтось міг би вживати std::functionабо лямбда - чому б не зробити це? Принаймні, це більш читаний синтаксис. Зазвичай потрібно використовувати вказівник функції для взаємодії з бібліотеками С (власне, з будь-якою зовнішньою бібліотекою) , і переконайтесь, що ви не можете переробити її, щоб прийняти std :: функцію або лямбда.
Привіт-Ангел

40

З лямбдаськими виразами, навіть захопленими, можна обробляти як покажчик функції (покажчик на функцію члена).

Це хитро, тому що лямбда-вираз - це не проста функція. Це фактично об'єкт з оператором ().

Коли ви творчі, ви можете використовувати це! Придумайте клас "функцій" у стилі std :: function. Якщо ви зберігаєте об'єкт, ви також можете використовувати вказівник функції.

Для використання покажчика функції ви можете використовувати наступне:

int first = 5;
auto lambda = [=](int x, int z) {
    return x + z + first;
};
int(decltype(lambda)::*ptr)(int, int)const = &decltype(lambda)::operator();
std::cout << "test = " << (lambda.*ptr)(2, 3) << std::endl;

Щоб побудувати клас, який може почати працювати як "std :: function", спочатку вам потрібен клас / структура, ніж може зберігати вказівник об'єкта та функції. Також вам потрібен оператор () для його виконання:

// OT => Object Type
// RT => Return Type
// A ... => Arguments
template<typename OT, typename RT, typename ... A>
struct lambda_expression {
    OT _object;
    RT(OT::*_function)(A...)const;

    lambda_expression(const OT & object)
        : _object(object), _function(&decltype(_object)::operator()) {}

    RT operator() (A ... args) const {
        return (_object.*_function)(args...);
    }
};

Завдяки цьому тепер ви можете запускати захоплені, не захоплені лямбда, як ви використовуєте оригінал:

auto capture_lambda() {
    int first = 5;
    auto lambda = [=](int x, int z) {
        return x + z + first;
    };
    return lambda_expression<decltype(lambda), int, int, int>(lambda);
}

auto noncapture_lambda() {
    auto lambda = [](int x, int z) {
        return x + z;
    };
    return lambda_expression<decltype(lambda), int, int, int>(lambda);
}

void refcapture_lambda() {
    int test;
    auto lambda = [&](int x, int z) {
        test = x + z;
    };
    lambda_expression<decltype(lambda), void, int, int>f(lambda);
    f(2, 3);

    std::cout << "test value = " << test << std::endl;
}

int main(int argc, char **argv) {
    auto f_capture = capture_lambda();
    auto f_noncapture = noncapture_lambda();

    std::cout << "main test = " << f_capture(2, 3) << std::endl;
    std::cout << "main test = " << f_noncapture(2, 3) << std::endl;

    refcapture_lambda();

    system("PAUSE");
    return 0;
}

Цей код працює з VS2015

Оновлення 04.07.17:

template <typename CT, typename ... A> struct function
: public function<decltype(&CT::operator())(A...)> {};

template <typename C> struct function<C> {
private:
    C mObject;

public:
    function(const C & obj)
        : mObject(obj) {}

    template<typename... Args> typename 
    std::result_of<C(Args...)>::type operator()(Args... a) {
        return this->mObject.operator()(a...);
    }

    template<typename... Args> typename 
    std::result_of<const C(Args...)>::type operator()(Args... a) const {
        return this->mObject.operator()(a...);
    }
};

namespace make {
    template<typename C> auto function(const C & obj) {
        return ::function<C>(obj);
    }
}

int main(int argc, char ** argv) {
   auto func = make::function([](int y, int x) { return x*y; });
   std::cout << func(2, 4) << std::endl;
   system("PAUSE");
   return 0;
}

Нічого це дивовижно! Тож ми просто могли використати внутрішні покажчики класу лямбда (до оператора функції члена ()) для виклику збережених лямбдів у класі обгортки !! ДИВОВИЖНИЙ!! Навіщо нам тоді потрібна функція std ::? І чи можна зробити lambda_expression <decltype (lambda), int, int, int> для автоматичного виведення / цих "int" параметрів безпосередньо з самої переданої лямбда?
barney

2
Я додав коротку версію власного коду. це має працювати з простою функцією auto f = make :: (лямбда); Але я дуже впевнений, що ви знайдете безліч ситуацій, коли мій код не буде працювати. std :: функція набагато краще побудована, ніж ця, і це має бути ходом, коли ви працюєте. Це тут для освіти та особистого користування.
Noxxer

14
Це рішення передбачає виклик лямбда через operator()реалізацію, тому, якщо я читаю це правильно, я не думаю, що було б справно викликати лямбда за допомогою функції вказівника у стилі С , чи не так? Саме це і було задано оригінальним запитанням.
Ремі Лебо

13
Ви заявляли, що лямбдами можна обробляти як функціональні вказівники, чого ви не робили. Ви створили ще один об'єкт для зберігання лямбда, який нічого не робить, ви могли просто використовувати оригінальну лямбду.
Перехожий

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

15

Захоплення лямбда не може бути перетворене на функціональні покажчики, як зазначено у цій відповіді .

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

static Callable callable;
static bool wrapper()
{
    return callable();
}

Це нудно. Ми продовжуємо цю ідею далі і автоматизуємо процес створення wrapperта полегшимо життя.

#include<type_traits>
#include<utility>

template<typename Callable>
union storage
{
    storage() {}
    std::decay_t<Callable> callable;
};

template<int, typename Callable, typename Ret, typename... Args>
auto fnptr_(Callable&& c, Ret (*)(Args...))
{
    static bool used = false;
    static storage<Callable> s;
    using type = decltype(s.callable);

    if(used)
        s.callable.~type();
    new (&s.callable) type(std::forward<Callable>(c));
    used = true;

    return [](Args... args) -> Ret {
        return Ret(s.callable(std::forward<Args>(args)...));
    };
}

template<typename Fn, int N = 0, typename Callable>
Fn* fnptr(Callable&& c)
{
    return fnptr_<N>(std::forward<Callable>(c), (Fn*)nullptr);
}

І використовувати його як

void foo(void (*fn)())
{
    fn();   
}

int main()
{
    int i = 42;
    auto fn = fnptr<void()>([i]{std::cout << i;});
    foo(fn);  // compiles!
}

Наживо

Це по суті оголошує анонімну функцію при кожному виникненні fnptr.

Зауважте, що виклики fnptrперезаписують раніше записані callableзадані дзвінки одного типу. Виправляємо це, певною мірою, за допомогою intпараметра N.

std::function<void()> func1, func2;
auto fn1 = fnptr<void(), 1>(func1);
auto fn2 = fnptr<void(), 2>(func2);  // different function

примушування оголосити ціле число N було б елегантним способом запам'ятати клієнта, щоб уникнути перезаписування покажчиків функцій під час компіляції.
fiorentinoing

2

Ярлик для використання лямбда з покажчиком функції С:

"auto fun = +[](){}"

Використання Curl в якості прикладу ( інформація про налагодження curl )

auto callback = +[](CURL* handle, curl_infotype type, char* data, size_t size, void*){ //add code here :-) };
curl_easy_setopt(curlHande, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curlHande,CURLOPT_DEBUGFUNCTION,callback);

3
Ця лямбда не має захоплення. Проблема ОП полягає в захопленні, не виведенні типу вказівника функції (саме в цьому +ви отримуєте трюк).
Sneftel

2

Хоча шаблонний підхід розумний з різних причин, важливо пам’ятати життєвий цикл лямбда та захоплені змінні. Якщо буде використана будь-яка форма лямбда-покажчика, і лямбда не є продовженням вниз, тоді слід використовувати лише копіюючу ламбда [=]. Тобто, навіть тоді захоплення вказівника на змінну на стеці - UNSAFE, якщо термін служби цих захоплених покажчиків (стек розмотується) коротший, ніж час життя лямбда.

Більш просте рішення для фіксації лямбда як вказівника:

auto pLamdba = new std::function<...fn-sig...>([=](...fn-sig...){...});

наприклад, new std::function<void()>([=]() -> void {...}

Просто пам’ятайте про пізніше, delete pLamdbaщоб переконатися, що вам не просочиться лямбда-пам’ять. Тут важливо розуміти, що лямбда можуть захоплювати лямбда (запитайте себе, як це працює), а також, щоб для std::functionроботи в цілому реалізація лямбда повинна містити достатню внутрішню інформацію, щоб забезпечити доступ до розміру даних лямбда (та захоплених) ( саме тому deleteповинні працювати [запущені деструктори захоплених типів]).


Навіщо турбуватися з newфункцією - std :: вже зберігає лямбда на купі І уникає необхідності запам’ятовувати виклики delete.
Кріс Додд

0

Не пряма відповідь, а незначна варіація використання шаблону шаблону «функтор», щоб приховати специфіку типу лямбда і зберегти код красивим і простим.

Я не був впевнений, як ви хочете використовувати клас вирішення, тому мені довелося розширити клас за допомогою функції, яка його використовує. Дивіться повний приклад тут: https://godbolt.org/z/jtByqE

Основна форма вашого класу може виглядати так:

template <typename Functor>
class Decide
{
public:
    Decide(Functor dec) : _dec{dec} {}
private:
    Functor _dec;
};

Де ви передаєте тип функції як частину типу класу, який використовується, як:

auto decide_fc = [](int x){ return x > 3; };
Decide<decltype(decide_fc)> greaterThanThree{decide_fc};

Знову ж таки, я не був впевнений, чому ти захоплюєш, xбуло більш доцільним (для мене) мати параметр, який передаєш лямбда), щоб ти міг використовувати:

int result = _dec(5); // or whatever value

Повний приклад див. За посиланням


-2

Як було зазначено іншими, ви можете замінити функцію Lambda замість покажчика функції. Я використовую цей метод у моєму інтерфейсі C ++ для розв'язувача F77 ODE RKSUITE.

//C interface to Fortran subroutine UT
extern "C"  void UT(void(*)(double*,double*,double*),double*,double*,double*,
double*,double*,double*,int*);

// C++ wrapper which calls extern "C" void UT routine
static  void   rk_ut(void(*)(double*,double*,double*),double*,double*,double*,
double*,double*,double*,int*);

//  Call of rk_ut with lambda passed instead of function pointer to derivative
//  routine
mathlib::RungeKuttaSolver::rk_ut([](double* T,double* Y,double* YP)->void{YP[0]=Y[1]; YP[1]= -Y[0];}, TWANT,T,Y,YP,YMAX,WORK,UFLAG);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.