C ++ lambda avec des captures comme pointeur de fonction

94

Je jouais avec les lambdas C ++ et leur conversion implicite en pointeurs de fonction. Mon exemple de départ les utilisait comme rappel pour la fonction ftw. Cela fonctionne comme prévu.

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

Après l'avoir modifié pour utiliser les captures:

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

J'ai eu l'erreur du compilateur:

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

Après quelques lectures. J'ai appris que les lambdas utilisant des captures ne peuvent pas être implicitement convertis en pointeurs de fonction.

Existe-t-il une solution de contournement pour cela? Le fait qu'ils ne peuvent pas être convertis «implicitement» signifie-t-il qu'ils peuvent être convertis «explicitement»? (J'ai essayé le casting, sans succès). Quelle serait une manière propre de modifier l'exemple de travail afin que je puisse ajouter les entrées à un objet à l'aide de lambdas?.

Duncan
la source
Quel compilateur utilisez-vous? est-ce VS10?
Ramon Zarazua B.21
gcc version 4.6.1 20110801 [gcc-4_6-branch révision 177033] (SUSE Linux)
duncan
4
Habituellement, la manière C de passer l'état aux callbacks se fait via un argument supplémentaire au callback (généralement de type void *). Si la bibliothèque que vous utilisez permet cet argument supplémentaire, vous trouverez une solution de contournement. Sinon, vous n'avez aucun moyen de réaliser proprement ce que vous voulez faire.
Alexandre C.
Oui. Je me rends compte que l'API de ftw.h et nftw.h est défectueuse. Je vais essayer fts.h
duncan
1
Génial! /usr/include/fts.h:41:3: erreur: #error "<fts.h> ne peut pas être utilisé avec -D_FILE_OFFSET_BITS == 64"
duncan

Réponses:

48

Puisque la capture de lambdas doit préserver un état, il n'y a pas vraiment de "contournement" simple, car ce ne sont pas que des fonctions ordinaires. Le point sur un pointeur de fonction est qu'il pointe vers une seule fonction globale et que cette information n'a pas de place pour un état.

La solution de contournement la plus proche (qui supprime essentiellement l'état) est de fournir un type de variable globale accessible à partir de votre fonction / lambda. Par exemple, vous pouvez créer un objet foncteur traditionnel et lui donner une fonction membre statique qui fait référence à une instance unique (globale / statique).

Mais cela va en quelque sorte à l'encontre de l'objectif de capturer des lambdas.

Kerrek SB
la source
3
Une solution plus propre consiste à envelopper le lambda dans un adaptateur, en supposant que le pointeur de fonction a un paramètre de contexte.
Raymond Chen
4
@RaymondChen: Eh bien, si vous êtes libre de définir comment la fonction doit être utilisée, alors oui, c'est une option. Mais dans ce cas, il serait encore plus facile de simplement faire du paramètre un argument du lambda lui-même!
Kerrek SB
3
@KerrekSB met les variables globales dans a namespaceet les marque comme thread_local, c'est l' ftwapproche que j'ai choisie pour résoudre quelque chose de similaire.
Kjell Hedström
"un pointeur de fonction pointe vers une seule fonction globale et cette information n'a pas de place pour un état." -> Comment diable des langages comme Java peuvent-ils accomplir cela alors? Eh bien, bien sûr, parce que cette fonction unique et globale est créée au moment de l'exécution et intègre l'état (ou plutôt la référence à celui-ci) dans son propre code. C'est toute la question - il ne devrait pas être une seule, la fonction globale , mais plusieurs fonctions globales - un pour chaque lambda est utilisé dans l' exécution. N'y a-t-il vraiment rien en C ++ qui fasse ça? (Je pensais que std :: function est fait exactement dans ce seul but)
Dexter
1
@Dexter: errr .. la réponse courte est non, la réponse longue implique une surcharge de l'opérateur. Quoi qu'il en soit, mon argument est valable. Java est un langage différent qui n'est pas le même que C ++; Java n'a pas de pointeurs (ou d'opérateurs d'appel surchargeables) et la comparaison ne fonctionne pas bien.
Kerrek SB
47

Je viens de rencontrer ce problème.

Le code se compile correctement sans capture lambda, mais il y a une erreur de conversion de type avec la capture lambda.

La solution avec C ++ 11 est d'utiliser std::function(modifier: une autre solution qui ne nécessite pas de modifier la signature de la fonction est affichée après cet exemple). Vous pouvez également utiliser boost::function(qui fonctionne en fait beaucoup plus rapidement). Exemple de code - modifié pour qu'il compile, compilé avec 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: J'ai dû revoir cela lorsque je suis tombé sur du code hérité où je ne pouvais pas modifier la signature de la fonction d'origine, mais j'avais toujours besoin d'utiliser des lambdas. Une solution qui ne nécessite pas de modifier la signature de fonction de la fonction d'origine est ci-dessous:

#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
la source
72
Non, cela ne devrait pas être la réponse acceptée. Le point ne change pas ftwpour prendre std::functionau lieu d'un pointeur de fonction ...
Gregory Pakosz
La deuxième solution proposée dans cette réponse répond à l'inquiétude de @ gregory-pakosz en préservant la signature d'origine, mais ce n'est toujours pas génial car elle introduit l'état global. Si ftwj'avais un argument void * userdata, alors je préférerais la réponse de @ evgeny-karpov.
prideout
@prideout était d'accord - je n'aime pas non plus l'état global. Malheureusement, en supposant que la signature de ftw ne peut pas être modifiée et étant donné qu'elle n'a pas de données utilisateur void *, l'état doit être stocké quelque part. J'ai rencontré ce problème en utilisant une bibliothèque tierce. Cela fonctionnera correctement tant que la bibliothèque ne capture pas le rappel et ne l'utilise pas plus tard, auquel cas la variable globale agit simplement comme un paramètre supplémentaire sur la pile d'appels. Si la signature de ftw peut être modifiée, alors je préférerais utiliser std :: function au lieu de void * userdata.
Jay West
1
c'est une solution extrêmement compliquée et utile, @Gregory je devrais vous dire "ça marche".
fiorentinoing
17

ORIGINAL

Les fonctions Lambda sont très pratiques et réduisent un code. Dans mon cas, j'avais besoin de lambdas pour la programmation parallèle. Mais cela nécessite des pointeurs de capture et de fonction. Ma solution est ici. Mais soyez prudent avec la portée des variables que vous avez capturées.

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

Exemple

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

Exemple avec une valeur de retour

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

METTRE À JOUR

Version améliorée

Cela faisait un moment que le premier article sur C ++ lambda avec des captures comme pointeur de fonction avait été publié. Comme il était utilisable pour moi et pour d'autres personnes, j'ai apporté des améliorations.

La fonction standard API du pointeur C utilise la convention void fn (void * data). Par défaut, cette convention est utilisée et lambda doit être déclaré avec un argument void *.

Mise en œuvre améliorée

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

Exemple

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

Conversion de lambda avec des captures en un pointeur C

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

Peut également être utilisé de cette façon

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

Au cas où la valeur de retour devrait être utilisée

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

Et dans le cas où des données sont utilisées

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
la source
3
C'est certainement la solution la plus pratique que j'ai vue pour convertir un lambda en un pointeur de fonction de style C. La fonction qui le prend comme argument aura juste besoin d'un paramètre supplémentaire représentant son état, souvent nommé "void * user" dans les bibliothèques C, afin de pouvoir le passer au pointeur de fonction lors de son appel.
Codoscope
10

En utilisant la méthode locale globale (statique), cela peut être fait comme suit

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

Supposons que nous ayons

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

Donc, l'utilisation sera

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

Cela fonctionne car chaque lambda a une signature unique, donc le rendre statique n'est pas un problème. Voici un wrapper générique avec un nombre variable d'arguments et tout type de retour utilisant la même méthode.

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

Et usage similaire

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
la source
1
sachez que cela copiera la fermeture (lors de l'obtention du ptr) + args (lors de l'appel). Sinon, c'est une solution élégante
Ivan Sanz-Carasa
bibliothèque d'aide en-tête uniquement: gist.github.com/isc30/fab67e5956fe8f2097bed84ebc42c1e8
Ivan Sanz-Carasa
1
@ IvanSanz-Carasa Merci d'avoir souligné. Les types de fermeture ne sont pas CopyAssignable, mais les foncteurs le sont. Alors vous avez raison, il vaut mieux utiliser la transmission parfaite ici. Pour les arguments d'un autre côté, nous ne pouvons pas faire grand-chose car le C brut ne supporte pas les références universelles, mais au moins nous pouvons renvoyer les valeurs à notre lambda. Cela peut enregistrer une copie supplémentaire. A modifié le code.
Vladimir Talybin
@RiaD Oui, parce que lambda une instance statique ici, vous devrez capturer par référence à la place, par exemple au lieu de l' =utiliser &idans votre boucle for.
Vladimir Talybin
5

Hehe - une question assez ancienne, mais quand même ...

#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;
}
égorse
la source
0

Il existe un moyen malveillant de convertir un lambda de capture en un pointeur de fonction, mais vous devez être prudent lorsque vous l'utilisez:

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

Votre code ressemblerait alors à ceci (avertissement: brain compile):

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
la source
0

Ma solution, utilisez simplement un pointeur de fonction pour faire référence à un lambda statique.

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
la source