Передача лямбда-захвата в качестве указателя на функцию


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] сказано ( выделено мое ):

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

Обратите внимание, что 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. потому что некоторые пути, может быть, то, что вы не хотите.
Негатив

1
Это не отвечает на вопрос. Если бы можно было использовать std::functionили лямбду - почему бы и нет? По крайней мере, это более читаемый синтаксис. Обычно необходимо использовать указатель на функцию для взаимодействия с библиотеками C (фактически, с любой внешней библиотекой) , и вы не можете изменить его, чтобы он принимал std :: function или lambda.
Привет-Ангел

40

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

Это сложно, потому что лямбда-выражение не простая функция. На самом деле это объект с оператором ().

Когда вы креативны, вы можете использовать это! Подумайте о классе «function» в стиле 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;
}

Вау, это удивительно! Таким образом, мы могли бы просто использовать внутренние указатели класса лямбды (на функцию-член operator ()) для вызова сохраненных лямбда-выражений в классе-обертке !! УДИВИТЕЛЬНЫЙ!! Зачем нам тогда нужна функция std ::? И можно ли заставить lambda_expression <decltype (lambda), int, int, int> автоматически выводить / эти "int" параметры непосредственно из самой передаваемой лямбды?
Барни

2
Я добавил короткую версию своего собственного кода. это должно работать с простым auto f = make :: function (lambda); Но я уверен, что вы найдете множество ситуаций, в которых мой код не будет работать. std :: function более хорошо сконструирован, чем этот, и его стоит использовать, когда вы работаете. Это здесь для образования и личного пользования.
Noxxer

14
Это решение включает вызов лямбды через operator()реализацию, поэтому, если я правильно ее читаю, я не думаю, что будет работать вызов лямбды с использованием указателя на функцию в стиле C , не так ли? Это то, что задал оригинальный вопрос.
Реми Лебо

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

Ярлык для использования лямбда с указателем на функцию C таков:

"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

Хотя шаблонный подход является умным по разным причинам, важно помнить о жизненном цикле лямбды и захваченных переменных. Если будет использоваться любая форма лямбда-указателя, и лямбда не является нисходящим продолжением, то должна использоваться только копирующая [=] лямбда. Т.е. даже тогда захват указателя на переменную в стеке НЕ БЕЗОПАСЕН, если время жизни этих захваченных указателей (разматывание стека) короче, чем время жизни лямбды.

Более простое решение для захвата лямбды в качестве указателя:

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

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

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


Зачем беспокоиться о new- std :: function уже хранит лямбда в куче И избегает необходимости запоминать вызов 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

Как уже упоминалось другими, вы можете заменить лямбда-функцию вместо указателя на функцию. Я использую этот метод в своем интерфейсе 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.