C ++ Lambda mit Captures als Funktionszeiger


93

Ich habe mit C ++ Lambdas und deren impliziter Konvertierung in Funktionszeiger gespielt. Mein Startbeispiel war, sie als Rückruf für die ftw-Funktion zu verwenden. Dies funktioniert wie erwartet.

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

Nach dem Ändern, um Captures zu verwenden:

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

Ich habe den Compilerfehler erhalten:

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

Nach einigem Lesen. Ich habe gelernt, dass Lambdas, die Captures verwenden, nicht implizit in Funktionszeiger konvertiert werden können.

Gibt es eine Problemumgehung dafür? Bedeutet die Tatsache, dass sie nicht "implizit" konvertiert werden können, dass sie "explizit" konvertiert werden können? (Ich habe versucht zu gießen, ohne Erfolg). Was wäre eine saubere Möglichkeit, das Arbeitsbeispiel so zu ändern, dass ich die Einträge mit Lambdas an ein Objekt anhängen kann?


Welchen Compiler verwenden Sie? ist es VS10?
Ramon Zarazua B.

gcc version 4.6.1 20110801 [gcc-4_6-branch revision 177033] (SUSE Linux)
duncan

4
Normalerweise erfolgt die C-Methode zum Übergeben des Status an Rückrufe über ein zusätzliches Argument an den Rückruf (normalerweise vom Typ void *). Wenn die von Ihnen verwendete Bibliothek dieses zusätzliche Argument zulässt, finden Sie eine Problemumgehung. Andernfalls haben Sie keine Möglichkeit, sauber zu erreichen, was Sie tun möchten.
Alexandre C.

Ja. Mir ist klar, dass die API von ftw.h und nftw.h fehlerhaft ist. Ich werde versuchen, fts.h
Duncan

1
Toll! /usr/include/fts.h:41:3: Fehler: #error "<fts.h> kann nicht mit -D_FILE_OFFSET_BITS == 64" verwendet werden
duncan

Antworten:


46

Da das Erfassen von Lambdas einen Zustand bewahren muss, gibt es keine wirklich einfache "Problemumgehung", da es sich nicht nur um gewöhnliche Funktionen handelt. Der Punkt bei einem Funktionszeiger ist, dass er auf eine einzelne globale Funktion verweist und diese Informationen keinen Platz für einen Status haben.

Die nächstgelegene Problemumgehung (die im Wesentlichen die Statefulness verwirft) besteht darin, eine Art globale Variable bereitzustellen, auf die von Ihrem Lambda / Ihrer Funktion aus zugegriffen wird. Sie können beispielsweise ein traditionelles Funktorobjekt erstellen und ihm eine statische Elementfunktion zuweisen, die auf eine eindeutige (globale / statische) Instanz verweist.

Aber das ist eine Art Niederlage gegen den gesamten Zweck, Lambdas zu fangen.


3
Eine sauberere Lösung besteht darin, das Lambda in einen Adapter zu packen, vorausgesetzt, der Funktionszeiger verfügt über einen Kontextparameter.
Raymond Chen

4
@RaymondChen: Nun, wenn Sie frei definieren können, wie die Funktion verwendet werden soll, dann ist dies eine Option. In diesem Fall wäre es jedoch noch einfacher, den Parameter nur zu einem Argument des Lambda selbst zu machen!
Kerrek SB

3
@KerrekSB setzt die globalen Variablen in a namespaceund markiert sie als thread_local, das ist der ftwAnsatz, den ich gewählt habe, um etwas Ähnliches zu lösen.
Kjell Hedström

"Ein Funktionszeiger zeigt auf eine einzelne globale Funktion, und diese Informationen bieten keinen Platz für einen Status." -> Wie zum Teufel können Sprachen wie Java das dann erreichen? Natürlich, weil diese einzelne globale Funktion zur Laufzeit erstellt wird und den Status (oder vielmehr den Verweis darauf) in ihren eigenen Code einbettet . Das ist der springende Punkt - es sollte nicht sein , einzelne, globale Funktion aber mehrere globalen Funktionen - ein für jede Zeit Lambda wird in Laufzeit verwendet. Gibt es wirklich NICHTS in C ++, das das tut? (Ich dachte, std :: function ist genau für diesen einen Zweck gemacht)
Dexter

1
@Dexter: errr .. die kurze Antwort ist nein, die lange Antwort beinhaltet eine Überlastung des Bedieners. Unabhängig davon steht mein Punkt. Java ist eine andere Sprache, die nicht mit C ++ identisch ist. Java hat keine Zeiger (oder überladbaren Aufrufoperatoren) und der Vergleich funktioniert nicht gut.
Kerrek SB

47

Ich bin gerade auf dieses Problem gestoßen.

Der Code wird ohne Lambda-Erfassung problemlos kompiliert, bei der Lambda-Erfassung tritt jedoch ein Typkonvertierungsfehler auf.

Die Lösung mit C ++ 11 ist die Verwendung std::function(Bearbeiten: Eine andere Lösung, bei der die Funktionssignatur nicht geändert werden muss, wird nach diesem Beispiel angezeigt). Sie können auch verwenden boost::function(was tatsächlich deutlich schneller läuft). Beispielcode - geändert, damit er kompiliert wird, kompiliert mit 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;
}

Bearbeiten: Ich musste dies erneut überprüfen, als ich auf alten Code stieß, bei dem ich die ursprüngliche Funktionssignatur nicht ändern konnte, aber dennoch Lambdas verwenden musste. Im Folgenden finden Sie eine Lösung, bei der die Funktionssignatur der ursprünglichen Funktion nicht geändert werden muss:

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

72
Nein, dies sollte nicht die akzeptierte Antwort sein. Der Punkt ändert sich nicht ftw, um std::functionanstelle eines Funktionszeigers zu nehmen ...
Gregory Pakosz

Die zweite in dieser Antwort vorgeschlagene Lösung behebt das Problem von @ gregory-pakosz, indem die ursprüngliche Signatur beibehalten wird. Sie ist jedoch immer noch nicht großartig, da sie den globalen Status einführt. Wenn ftwich ein void * userdata-Argument hätte, würde ich die Antwort von @ evgeny-karpov vorziehen.
Stolz

@prideout stimmte zu - ich mag den globalen Staat auch nicht. Unter der Annahme, dass die Signatur von ftw nicht geändert werden kann und keine void * userdata enthält, muss der Status leider irgendwo gespeichert werden. Ich bin auf dieses Problem mit einer Bibliothek eines Drittanbieters gestoßen. Dies funktioniert einwandfrei, solange die Bibliothek den Rückruf nicht erfasst und später verwendet. In diesem Fall verhält sich die globale Variable einfach wie ein zusätzlicher Parameter im Aufrufstapel. Wenn die Signatur von ftw geändert werden kann, würde ich lieber die Funktion std :: anstelle von void * userdata verwenden.
Jay West

1
Dies ist eine äußerst komplizierte und nützliche Lösung. @Gregory Ich sollte Ihnen sagen, dass es funktioniert.
Fiorentinoing

16

ORIGINAL

Lambda-Funktionen sind sehr praktisch und reduzieren einen Code. In meinem Fall brauchte ich Lambdas für die parallele Programmierung. Es erfordert jedoch Erfassungs- und Funktionszeiger. Meine Lösung ist hier. Seien Sie jedoch vorsichtig mit dem Umfang der Variablen, die Sie erfasst haben.

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

Beispiel

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

Beispiel mit einem Rückgabewert

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

AKTUALISIEREN

Verbesserte Version

Es ist eine Weile her, dass der erste Beitrag über C ++ Lambda mit Captures als Funktionszeiger veröffentlicht wurde. Da es für mich und andere Leute brauchbar war, habe ich einige Verbesserungen vorgenommen.

Die C-Zeiger-API der Standardfunktion verwendet die Konvention void fn (void * data). Standardmäßig wird diese Konvention verwendet und Lambda sollte mit einem void * -Argument deklariert werden.

Verbesserte Implementierung

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

Beispiel

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

Konvertieren von Lambda mit Captures in einen C-Zeiger

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

Kann auch so verwendet werden

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

Falls der Rückgabewert verwendet werden sollte

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

Und falls Daten verwendet werden

auto b2 = [&](void* data) {return *(int*)(data) + a;};
int (*f4)(void*) = Lambda::ptr<int>(b2);
int data = 5;
printf("%d\n", f4(&data)); // 108

3
Dies ist definitiv die bequemste Lösung, die ich gesehen habe, um ein Lambda in einen Funktionszeiger im C-Stil umzuwandeln. Die Funktion, die es als Argument verwendet, benötigt lediglich einen zusätzlichen Parameter, der ihren Status darstellt und in C-Bibliotheken häufig als "void * user" bezeichnet wird, damit sie ihn beim Aufruf an den Funktionszeiger übergeben kann.
Codoskop

10

Mit der lokal globalen (statischen) Methode kann wie folgt vorgegangen werden

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

Angenommen, wir haben

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

So wird die Nutzung sein

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

Dies funktioniert, weil jedes Lambda eine eindeutige Signatur hat, sodass es kein Problem ist, es statisch zu machen. Es folgt ein generischer Wrapper mit einer variablen Anzahl von Argumenten und einem beliebigen Rückgabetyp, der dieselbe Methode verwendet.

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

Und ähnliche Verwendung

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

1
Beachten Sie, dass dadurch der Abschluss (beim Abrufen des ptr) + args (beim Aufrufen) kopiert wird. Ansonsten ist es eine elegante Lösung
Ivan Sanz-Carasa


1
@ IvanSanz-Carasa Danke für den Hinweis. Verschlusstypen sind nicht CopyAssignable, Funktoren jedoch. Sie haben Recht, es ist besser, hier eine perfekte Weiterleitung zu verwenden. Für Argumente hingegen können wir nicht viel tun, da einfaches C keine universellen Referenzen unterstützt, aber zumindest können wir Werte an unser Lambda zurückleiten. Dadurch wird möglicherweise eine zusätzliche Kopie gespeichert. Hat Code bearbeitet.
Vladimir Talybin

@RiaD Ja, weil Lambda eine statische Instanz hier finden Sie unter Bezugnahme auf Capture müssen stattdessen zB statt =Verwendung &iin der for-Schleife.
Vladimir Talybin

5

Hehe - eine ziemlich alte Frage, aber immer noch ...

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

0

Es gibt eine hackige Möglichkeit, ein Erfassungs-Lambda in einen Funktionszeiger umzuwandeln, aber Sie müssen vorsichtig sein, wenn Sie es verwenden:

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

Ihr Code würde dann so aussehen (Warnung: Gehirn kompilieren):

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

0

Meine Lösung, verwenden Sie einfach einen Funktionszeiger, um auf ein statisches Lambda zu verweisen.

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

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.