C ++ lambda con capturas como puntero de función

94

Estaba jugando con lambdas de C ++ y su conversión implícita a punteros de función. Mi ejemplo inicial fue usarlos como devolución de llamada para la función ftw. Esto funciona como se esperaba.

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

Después de modificarlo 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;
}

Recibí el error del 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)’

Después de leer un poco. Aprendí que las lambdas que usan capturas no se pueden convertir implícitamente en punteros de función.

¿Hay una solución para esto? ¿El hecho de que no se puedan convertir "implícitamente" significa que pueden convertirse "explícitamente"? (Intenté lanzar, sin éxito). ¿Cuál sería una forma limpia de modificar el ejemplo de trabajo para poder agregar las entradas a algún objeto usando lambdas?

Duncan
fuente
Qué compilador estas usando? es VS10?
Ramon Zarazua B.
gcc versión 4.6.1 20110801 [gcc-4_6-branch revision 177033] (SUSE Linux)
duncan
4
Por lo general, la forma en C de pasar el estado a las devoluciones de llamada se realiza a través de un argumento adicional a la devolución de llamada (generalmente de tipo void *). Si la biblioteca que está utilizando permite este argumento adicional, encontrará una solución. De lo contrario, no tiene forma de lograr limpiamente lo que quiere hacer.
Alexandre C.
Si. Me doy cuenta de que la API de ftw.hy nftw.h tiene fallas. Voy a tratar fts.h
Duncan
1
¡Excelente! /usr/include/fts.h:41:3: error: #error "<fts.h> no se puede usar con -D_FILE_OFFSET_BITS == 64"
duncan

Respuestas:

48

Dado que la captura de lambdas necesita preservar un estado, no existe realmente una "solución alternativa" simple, ya que no son solo funciones ordinarias. El punto sobre un puntero de función es que apunta a una única función global, y esta información no tiene espacio para un estado.

La solución alternativa más cercana (que esencialmente descarta el estado) es proporcionar algún tipo de variable global a la que se accede desde su función lambda /. Por ejemplo, podría crear un objeto functor tradicional y darle una función miembro estática que se refiere a alguna instancia única (global / estática).

Pero eso es como frustrar todo el propósito de capturar lambdas.

Kerrek SB
fuente
3
Una solución más limpia es envolver el lambda dentro de un adaptador, asumiendo que el puntero de función tiene un parámetro de contexto.
Raymond Chen
4
@RaymondChen: Bueno, si tiene la libertad de definir cómo se usará la función, entonces sí, esa es una opción. Aunque en ese caso sería aún más fácil convertir el parámetro en un argumento de la lambda misma.
Kerrek SB
3
@KerrekSB puso las variables globales en a namespacey las marcó como thread_local, ese es el ftwenfoque que elegí para resolver algo similar.
Kjell Hedström
"un puntero de función apunta a una única función global, y esta información no tiene espacio para un estado". -> ¿Cómo diablos pueden entonces los lenguajes como Java lograr esto? Bueno, por supuesto, porque esa única función global se crea en tiempo de ejecución e incrusta el estado (o más bien la referencia a él) en su propio código. Ese es el punto entero - no debe no ser una sola función, global , sino múltiples funciones globales - una para cada lambda vez que se utiliza en tiempo de ejecución. ¿Realmente no hay NADA en C ++ que haga eso? (Pensé que la función std :: está hecha exactamente para ese único propósito)
Dexter
1
@Dexter: errr ... la respuesta corta es no, la respuesta larga implica sobrecarga del operador. Independientemente, mi punto se mantiene. Java es un lenguaje diferente que no es lo mismo que C ++; Java no tiene punteros (ni operadores de llamadas sobrecargadas) y la comparación no funciona bien.
Kerrek SB
47

Me acabo de encontrar con este problema.

El código se compila bien sin capturas lambda, pero hay un error de conversión de tipo con captura lambda.

La solución con C ++ 11 es usar std::function(editar: se muestra otra solución que no requiere modificar la firma de la función después de este ejemplo). También puede usar boost::function(que en realidad se ejecuta significativamente más rápido). Código de ejemplo: cambiado para que se compile, compilado con 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;
}

Editar: tuve que volver a visitar esto cuando me encontré con un código heredado en el que no podía modificar la firma de la función original, pero aún necesitaba usar lambdas. A continuación se muestra una solución que no requiere modificar la firma de la función original:

#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
fuente
72
No, esta no debería ser la respuesta aceptada. El punto no está cambiando ftwpara tomar en std::functionlugar de un puntero de función ...
Gregory Pakosz
La segunda solución propuesta dentro de esta respuesta aborda la preocupación de @ gregory-pakosz al preservar la firma original, pero aún no es excelente porque presenta el estado global. Si ftwtuviera un argumento vacío * userdata, entonces preferiría la respuesta de @ evgeny-karpov.
orgullo
@prideout estuvo de acuerdo: tampoco me gusta el estado global. Desafortunadamente, asumiendo que la firma de ftw no se puede modificar y dado que no tiene datos de usuario void *, el estado debe almacenarse en algún lugar. Me encontré con este problema usando una biblioteca de terceros. Esto funcionará bien siempre que la biblioteca no capture la devolución de llamada y la use más tarde, en cuyo caso la variable global simplemente actúa como un parámetro adicional en la pila de llamadas. Si se puede modificar la firma de ftw, entonces preferiría usar std :: function en lugar de void * userdata.
Jay West
1
esta es una solución extremadamente complicada y útil, @ Gregory, debería decirte "funciona".
fiorentinoing
17

ORIGINAL

Las funciones Lambda son muy convenientes y reducen un código. En mi caso, necesitaba lambdas para programación paralela. Pero requiere punteros de captura y función. Mi solucion esta aqui. Pero tenga cuidado con el alcance de las variables que capturó.

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

Ejemplo

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

Ejemplo con valor de retorno

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

ACTUALIZAR

Versión mejorada

Ha pasado un tiempo desde que se publicó la primera publicación sobre lambda de C ++ con capturas como puntero de función. Como era utilizable para mí y para otras personas, hice algunas mejoras.

La API de puntero C de función estándar usa la convención void fn (void * data). De forma predeterminada, se usa esta convención y lambda debe declararse con un argumento void *.

Implementación mejorada

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

Ejemplo

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

Conversión de lambda con capturas a un puntero C

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

También se puede usar de esta manera

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

En caso de que se deba usar el valor de retorno

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

Y en caso de que se utilicen datos

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
fuente
3
Esta es definitivamente la solución más conveniente que he visto para convertir una lambda en un puntero de función de estilo C. La función que lo toma como argumento solo necesitará un parámetro adicional que represente su estado, a menudo llamado "usuario void *" en las bibliotecas de C, para que pueda pasarlo al puntero de la función cuando lo llame.
Codoscope
10

Usando el método local global (estático) se puede hacer de la siguiente manera

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

Supongamos que tenemos

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

Entonces el uso será

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

Esto funciona porque cada lambda tiene una firma única, por lo que hacerla estática no es un problema. A continuación se muestra un contenedor genérico con un número variable de argumentos y cualquier tipo de retorno utilizando el mismo 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));
}

Y uso similar

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
fuente
1
tenga en cuenta que esto copiará el cierre (al obtener el ptr) + args (al llamar). De lo contrario, es una solución elegante
Ivan Sanz-Carasa
biblioteca auxiliar solo de encabezado: gist.github.com/isc30/fab67e5956fe8f2097bed84ebc42c1e8
Ivan Sanz-Carasa
1
@ IvanSanz-Carasa Gracias por señalarlo. Los tipos de cierre no son CopyAssignable, pero los functors sí lo son. Así que tienes razón, es mejor usar el reenvío perfecto aquí. Por otro lado, para los argumentos no podemos hacer mucho, ya que C simple no admite referencias universales, pero al menos podemos reenviar valores a nuestra lambda. Esto puede ahorrarle una copia adicional. Tiene código editado.
Vladimir Talybin
@RiaD Sí, porque lambda es una instancia estática aquí, necesitará capturar por referencia en su lugar, por ejemplo, en lugar de =usar &ien su bucle for.
Vladimir Talybin
5

Jeje, una pregunta bastante antigua, pero aún así ...

#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
fuente
0

Existe una forma hackear de convertir un lambda de captura en un puntero de función, pero debe tener cuidado al usarlo:

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

Su código se vería así (advertencia: compilación del cerebro):

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;
}
usuario1095108
fuente
0

Mi solución, solo use un puntero de función para referirse a una lambda estática.

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
fuente