Passando capturando lambda como ponteiro de função

210

É possível passar uma função lambda como um ponteiro de função? Nesse caso, devo estar fazendo algo incorretamente porque estou recebendo um erro de compilação.

Considere o seguinte exemplo

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

Quando tento compilar isso , recebo o seguinte erro de compilação:

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&&'

Essa é uma mensagem de erro para digerir, mas acho que o que estou tirando disso é que o lambda não pode ser tratado como um constexprmodo, portanto, não posso transmiti-lo como um ponteiro de função? Eu tentei fazer xconst também, mas isso não parece ajudar.

Cory Kramer
fonte
34
O lambda pode se deteriorar para funcionar como ponteiro apenas se não capturar nada.
Jarod42
Para a posteridade, a postagem do blog acima link
warrenm

Respostas:

205

Um lambda só pode ser convertido em um ponteiro de função se não capturar, como mostra a seção padrão do C ++ 11 5.1.2 [expr.prim.lambda] ( ênfase minha ):

O tipo de fechamento para uma expressão lambda sem captura lambda possui uma função pública de conversão const não virtual não explícita em ponteiro para função com o mesmo parâmetro e tipos de retorno que o operador de chamada de função do tipo de fechamento. O valor retornado por essa função de conversão deve ser o endereço de uma função que, quando chamada, tem o mesmo efeito que a chamada do operador de chamada de função do tipo de fechamento.

Observe que o cppreference também aborda isso em sua seção sobre funções do Lambda .

Portanto, as seguintes alternativas funcionariam:

typedef bool(*DecisionFn)(int);

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

e assim seria:

typedef bool(*DecisionFn)();

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

e como 5gon12eder aponta, você também pode usá-lo std::function, mas observe que std::functioné muito pesado , portanto, não é uma troca barata.

Shafik Yaghmour
fonte
2
Nota lateral: Uma solução comum usada pelo material C é passar a void*como o único parâmetro. É normalmente chamado de "ponteiro do usuário". Também é relativamente leve, mas tende a exigir mallocalgum espaço.
Fund Monica's Lawsuit
94

A resposta de Shafik Yaghmour explica corretamente por que o lambda não pode ser passado como ponteiro de função se tiver uma captura. Eu gostaria de mostrar duas correções simples para o problema.

  1. Use em std::functionvez de ponteiros de função bruta.

    Esta é uma solução muito limpa. Observe, no entanto, que ele inclui alguma sobrecarga adicional para o apagamento do tipo (provavelmente uma chamada de função virtual).

    #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. Use uma expressão lambda que não captura nada.

    Como seu predicado é realmente apenas uma constante booleana, o seguinte rapidamente resolveria o problema atual. Veja esta resposta para uma boa explicação de por que e como isso está funcionando.

    // Your 'Decide' class as in your post.
    
    int
    main()
    {
      int x = 5;
      Decide greaterThanThree {
        (x > 3) ? [](){ return true; } : [](){ return false; }
      };
    }
5gon12eder
fonte
4
@TC Veja esta questão para mais detalhes por que ela funciona
Shafik Yaghmour
Observe que, em geral, se você conhece os dados de captura em tempo de compilação, pode convertê-los em dados de tipo e voltar a ter um lambda sem captura - veja esta resposta que acabei de escrever para outra pergunta (graças a @ Resposta de 5gon12eder aqui).
Dan-man
O objeto não deveria ter uma vida útil mais longa que a função ponteiro? Eu gostaria de usá-lo para glutReshapeFunc.
Ar2015 31/08/19
eu não recomendo esta sugestão, coisas que tendem a funcionar magicamente, introduzem novos erros. e práticas que acompanham esses erros. se você quiser usar std :: function, deverá ver todos os tipos de maneiras pelas quais std :: function pode ser usado. porque algumas maneiras talvez algo que você não quer.
TheNegative 07/07/19
1
Isso não responde à pergunta. Se alguém poderia usar std::functionou um lambda - por que não? No mínimo, é uma sintaxe mais legível. Normalmente, é necessário usar um ponteiro de função para interagir com as bibliotecas C (na verdade, com qualquer biblioteca externa) , e você não pode modificá-lo para aceitar uma função std :: ou lambda.
Hi-Angel
40

Expressões lambda, mesmo as capturadas, podem ser manipuladas como um ponteiro de função (ponteiro para a função membro).

É complicado porque uma expressão lambda não é uma função simples. Na verdade, é um objeto com um operador ().

Quando você é criativo, pode usar isso! Pense em uma classe "function" no estilo de std :: function. Se você salvar o objeto, também poderá usar o ponteiro de função.

Para usar o ponteiro de função, você pode usar o seguinte:

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;

Para criar uma classe que possa começar a funcionar como uma "std :: function", primeiro você precisa de uma classe / estrutura que possa armazenar o objeto e o ponteiro de função. Você também precisa de um operador () para executá-lo:

// 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...);
    }
};

Com isso, agora você pode executar lambdas capturadas e não capturadas, exatamente como você está usando o original:

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

Este código funciona com o VS2015

Atualização 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;
}
Noxxer
fonte
Wow isso é incrível! Portanto, poderíamos usar os ponteiros internos da classe lambda (para operador de função membro ()) para invocar lambdas armazenadas em uma classe wrapper !! SURPREENDENTE!! Por que precisamos sempre da função std :: então? E é possível fazer com que lambda_expression <decltype (lambda), int, int, int> deduza automaticamente / esses parâmetros "int" diretamente do próprio lambda transmitido?
Barney #
2
Eu adicionei uma versão curta do meu próprio código. isso deve estar funcionando com um simples auto f = make :: function (lambda); Mas tenho certeza que você encontrará muitas situações em que meu código não funcionará. A função std :: é muito mais bem construída que essa e deve ser a opção quando você estiver trabalhando. Isto aqui é para educação e uso pessoal.
Noxxer 04/07/19
14
Essa solução envolve chamar o lambda por meio de uma operator()implementação; portanto, se eu estiver lendo direito, não acho que funcionaria chamar o lambda usando um ponteiro de função no estilo C , seria? Foi isso que a pergunta original pediu.
Remy Lebeau
13
Você afirmou que lambdas podem ser tratadas como ponteiros de função, o que você não fez. Você criou outro objeto para armazenar uma lambda, que não faz nada; você poderia apenas usar a lambda original.
De
9
Isso não é "passar capturando lambda como ponteiro de função". Isso é "passar capturando lambda como um objeto que contém um ponteiro de função entre outras coisas". Há um mundo de diferença.
n. 'pronomes' m.
15

A captura de lambdas não pode ser convertida em ponteiros de função, pois esta resposta apontou.

No entanto, muitas vezes é muito difícil fornecer um ponteiro de função para uma API que aceita apenas um. O método mais citado para fazer isso é fornecer uma função e chamar um objeto estático.

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

Isso é entediante. Levamos essa idéia adiante e automatizamos o processo de criação wrappere facilitar a vida.

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

E use-o como

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

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

Viver

Isto é essencialmente declarar uma função anônima em cada ocorrência de fnptr .

Observe que as invocações de fnptrsobrescrever os callablecallables fornecidos anteriormente escritos do mesmo tipo. Nós remediar esta situação, até certo ponto, com o intparâmetro N.

std::function<void()> func1, func2;
auto fn1 = fnptr<void(), 1>(func1);
auto fn2 = fnptr<void(), 2>(func2);  // different function
Passer By
fonte
forçar o número inteiro N a ser declarado seria uma maneira elegante de lembrar o cliente para evitar a substituição dos ponteiros de função no tempo de compilação.
fiorentinoing
2

Um atalho para usar um lambda como um ponteiro de função C é este:

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

Usando Curl como exemplo ( informações de depuração de 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);
janCoffee
fonte
3
Esse lambda não tem uma captura. O problema do OP é a captura, não sendo necessário deduzir o tipo de ponteiro de função (que é esse o +truque que você recebe).
Sneftel 15/05/19
2

Embora a abordagem do modelo seja inteligente por vários motivos, é importante lembrar o ciclo de vida do lambda e as variáveis ​​capturadas. Se alguma forma de ponteiro lambda for usada e o lambda não for uma continuação descendente, somente um lambda de cópia [=] deve ser usado. Ou seja, mesmo assim, capturar um ponteiro para uma variável na pilha não é SEGURO se a vida útil desses ponteiros capturados (desenrolar a pilha) for menor que a vida útil da lambda.

Uma solução mais simples para capturar um lambda como um ponteiro é:

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

por exemplo, new std::function<void()>([=]() -> void {...}

Lembre-se de mais tarde, delete pLamdbapara garantir que você não vaze a memória lambda. O segredo para perceber aqui é que os lambdas podem capturar lambdas (pergunte a si mesmo como isso funciona) e também que, para std::functionfuncionar genericamente, a implementação lambda precisa conter informações internas suficientes para fornecer acesso ao tamanho dos dados lambda (e capturados) ( é por isso que deletedeve funcionar [executando destruidores de tipos capturados]).

smallscript
fonte
Por que se preocupar com a newfunção - std :: já armazena o lambda na pilha E evita a necessidade de lembrar de chamar delete.
Chris Dodd
0

Não é uma resposta direta, mas uma pequena variação para usar o padrão de modelo "functor" para ocultar as especificidades do tipo lambda e manter o código agradável e simples.

Eu não tinha certeza de como você queria usar a classe decide, então tive que estender a classe com uma função que a utiliza. Veja o exemplo completo aqui: https://godbolt.org/z/jtByqE

A forma básica da sua classe pode ser assim:

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

Onde você passa o tipo da função como parte do tipo de classe usado como:

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

Mais uma vez, eu não sabia por que você xestava capturando , fazia mais sentido (para mim) ter um parâmetro que você passasse para o lambda) para que você pudesse usar como:

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

Veja o link para um exemplo completo

code_fodder
fonte
-2

Como foi mencionado pelos outros, você pode substituir a função Lambda em vez do ponteiro de função. Eu estou usando esse método na minha interface C ++ para o solucionador de 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);
beniekg
fonte