C ++ lambda com capturas como um ponteiro de função

94

Eu estava brincando com lambdas C ++ e sua conversão implícita em ponteiros de função. Meu exemplo inicial foi usá-los como retorno de chamada para a função ftw. Isso funciona conforme o esperado.

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

Depois de modificá-lo para usar capturas:

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

Recebi o erro do compilador:

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)’

Depois de alguma leitura. Aprendi que lambdas usando capturas não podem ser convertidos implicitamente em ponteiros de função.

Existe uma solução alternativa para isso? O fato de não poderem ser convertidos "implicitamente" significa que podem ser convertidos "explicitamente"? (Tentei lançar, sem sucesso). Qual seria uma maneira limpa de modificar o exemplo de trabalho para que eu pudesse anexar as entradas a algum objeto usando lambdas ?.

duncan
fonte
Qual compilador você está usando? é VS10?
Ramon Zarazua B.
gcc versão 4.6.1 20110801 [gcc-4_6-branch revisão 177033] (SUSE Linux)
duncan
4
Normalmente, a maneira C de passar estado para retornos de chamada é feita por meio de um argumento extra para o retorno de chamada (geralmente do tipo void *). Se a biblioteca que você está usando permitir esse argumento extra, você encontrará uma solução alternativa. Do contrário, você não tem como conseguir de forma limpa o que deseja fazer.
Alexandre C.
Sim. Percebo que a API de ftw.he nftw.h está com defeito. Vou tentar fts.h
duncan
1
Ótimo! /usr/include/fts.h:41:3: erro: #error "<fts.h> não pode ser usado com -D_FILE_OFFSET_BITS == 64"
duncan

Respostas:

47

Uma vez que a captura de lambdas precisa preservar um estado, não há realmente uma "solução alternativa" simples, já que elas não são apenas funções comuns. A questão sobre um ponteiro de função é que ele aponta para uma única função global e essa informação não tem espaço para um estado.

A solução alternativa mais próxima (que essencialmente descarta o estado) é fornecer algum tipo de variável global que é acessada de sua função / lambda. Por exemplo, você poderia fazer um objeto functor tradicional e dar a ele uma função de membro estática que se refere a alguma instância única (global / estática).

Mas isso meio que anula todo o propósito de capturar lambdas.

Kerrek SB
fonte
3
Uma solução mais limpa é envolver o lambda dentro de um adaptador, supondo que o ponteiro de função tenha um parâmetro de contexto.
Raymond Chen
4
@RaymondChen: Bem, se você tem liberdade para definir como a função deve ser usada, então sim, essa é uma opção. Embora, nesse caso, seja ainda mais fácil apenas tornar o parâmetro um argumento do próprio lambda!
Kerrek SB
3
@KerrekSB coloque as variáveis ​​globais em a namespacee marque-as como thread_local, essa é a ftwabordagem que escolhi para resolver algo semelhante.
Kjell Hedström
"um ponteiro de função aponta para uma única função global e essa informação não tem espaço para um estado." -> Como diabos linguagens como Java podem fazer isso então? Bem, é claro, porque essa função única e global é criada em tempo de execução e incorpora o estado (ou melhor, a referência a ele) em seu próprio código. Esse é o ponto inteiro - não deve não ser uma única função, global , mas várias funções globais - um para cada lambda tempo é usado em tempo de execução. Não há realmente NADA em C ++ que faça isso? (Achei que std :: function fosse feito exatamente para esse propósito)
Dexter
1
@Dexter: errr .. a resposta curta é não, a resposta longa envolve sobrecarga do operador. Independentemente disso, meu ponto permanece. Java é uma linguagem diferente que não é igual a C ++; Java não tem ponteiros (ou operadores de chamada sobrecarregados) e a comparação não funciona bem.
Kerrek SB
47

Acabei de encontrar esse problema.

O código compila bem sem capturas lambda, mas há um erro de conversão de tipo com captura lambda.

A solução com C ++ 11 é usar std::function(editar: outra solução que não requer modificação da assinatura da função é mostrada após este exemplo). Você também pode usar boost::function(que na verdade é muito mais rápido). Código de exemplo - alterado para que seja compilado, compilado com 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;
}

Edit: Eu tive que revisitar isso quando encontrei o código legado onde não pude modificar a assinatura da função original, mas ainda precisava usar lambdas. Uma solução que não requer a modificação da assinatura da função original está abaixo:

#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;
}
Jay West
fonte
72
Não, esta não deve ser a resposta aceita. O ponto não está mudando ftwpara tomar em std::functionvez de um ponteiro de função ...
Gregory Pakosz
A segunda solução proposta nesta resposta aborda a preocupação de @gregory-pakosz ao preservar a assinatura original, mas ainda não é ótima porque introduz um estado global. Se ftwtivesse um argumento void * userdata, então eu preferiria a resposta de @ evgeny-karpov.
prideout
@prideout concordou - eu também não gosto do estado global. Infelizmente, assumindo que a assinatura do ftw não pode ser modificada e dado que não tem void * userdata, o estado tem que ser armazenado em algum lugar. Eu tive esse problema usando uma biblioteca de terceiros. Isso funcionará bem, desde que a biblioteca não capture o retorno de chamada e o use mais tarde; nesse caso, a variável global simplesmente atua como um parâmetro extra na pilha de chamadas. Se a assinatura do ftw puder ser modificada, eu prefiro usar std :: function ao invés de void * userdata.
Jay West
1
esta é uma solução extremamente complicada e útil, @Gregory devo dizer que "funciona".
fiorentinoing
16

ORIGINAL

As funções Lambda são muito convenientes e reduzem um código. No meu caso, precisei de lambdas para programação paralela. Mas requer captura e ponteiros de função. Minha solução está aqui. Mas tome cuidado com o escopo das variáveis ​​que você capturou.

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

Exemplo

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

Exemplo com um valor de retorno

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

ATUALIZAR

Versão melhorada

Já faz um tempo que foi postado o primeiro post sobre lambda C ++ com capturas como um ponteiro de função. Como era utilizável para mim e outras pessoas, fiz algumas melhorias.

A função padrão C pointer api usa a convenção void fn (void * data). Por padrão, essa convenção é usada e lambda deve ser declarado com um argumento void *.

Implementação aprimorada

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

Convertendo lambda com capturas em um ponteiro C

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

Pode ser usado dessa forma também

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

Caso o valor de retorno deva ser usado

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

E caso os dados sejam usados

auto b2 = [&](void* data) {return *(int*)(data) + a;};
int (*f4)(void*) = Lambda::ptr<int>(b2);
int data = 5;
printf("%d\n", f4(&data)); // 108
Evgeny Karpov
fonte
3
Esta é definitivamente a solução mais conveniente que vi para converter um lambda em um ponteiro de função estilo C. A função que o toma como um argumento só precisa de um parâmetro extra representando seu estado, geralmente denominado "void * user" nas bibliotecas C, para que possa passá-lo para o ponteiro de função ao chamá-lo.
Codoscópio
10

Usando o método localmente global (estático), pode ser feito da seguinte maneira

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

Suponha que temos

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

Então, o uso será

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

Isso funciona porque cada lambda tem uma assinatura exclusiva, portanto torná-la estática não é um problema. A seguir está um wrapper genérico com número variável de argumentos e qualquer tipo de retorno usando o mesmo método.

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

E uso semelhante

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;
}));
Vladimir Talybin
fonte
1
esteja ciente de que isso copiará o encerramento (ao obter o ptr) + args (ao chamar). Caso contrário, é uma solução elegante
Ivan Sanz-Carasa
biblioteca auxiliar somente de cabeçalho: gist.github.com/isc30/fab67e5956fe8f2097bed84ebc42c1e8
Ivan Sanz-Carasa
1
@ IvanSanz-Carasa Obrigado por apontar. Os tipos de fechamento não são CopyAssignable, mas os functores são. Então está certo, é melhor usar o encaminhamento perfeito aqui. Por outro lado, para args, não podemos fazer muito, já que o C simples não oferece suporte a referências universais, mas pelo menos podemos encaminhar os valores de volta para nosso lambda. Isso pode salvar uma cópia extra. Editou código.
Vladimir Talybin
@RiaD Sim, porque lambda é uma instância estática aqui, você precisará capturar por referência, por exemplo, em vez de =usar &iem seu loop for.
Vladimir Talybin
5

Hehe - uma questão bastante antiga, mas ainda assim ...

#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;
}
egorse
fonte
0

Existe uma maneira hackeada de converter um lambda de captura em um ponteiro de função, mas você precisa ter cuidado ao usá-lo:

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

Seu código ficaria assim (aviso: compilação do cérebro):

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;
}
user1095108
fonte
0

Minha solução, basta usar um ponteiro de função para se referir a um lambda estático.

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();
}
Zhang
fonte