C ++ lambda z przechwytywaniami jako wskaźnikiem funkcji

94

Bawiłem się lambdami C ++ i ich niejawną konwersją na wskaźniki funkcji. Moim początkowym przykładem było użycie ich jako wywołania zwrotnego dla funkcji ftw. Działa to zgodnie z oczekiwaniami.

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

Po zmodyfikowaniu go w celu użycia przechwytywania:

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

Otrzymałem błąd kompilatora:

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)

Po lekturze. Dowiedziałem się, że lambdy używające przechwytywania nie mogą być niejawnie konwertowane na wskaźniki funkcji.

Czy istnieje obejście tego problemu? Czy fakt, że nie można ich „niejawnie” przekonwertować, oznacza, że ​​można je „jawnie” przekonwertować? (Próbowałem rzucać, ale bez powodzenia). Jaki byłby czysty sposób zmodyfikowania działającego przykładu, aby móc dołączyć wpisy do jakiegoś obiektu za pomocą lambd?

duncan
źródło
Jakiego kompilatora używasz? czy to VS10?
Ramon Zarazua B.
wersja gcc 4.6.1 20110801 [wersja gcc-4_6-branch 177033] (SUSE Linux)
duncan
4
Zwykle sposób przekazywania stanu w języku C do wywołań zwrotnych odbywa się za pośrednictwem dodatkowego argumentu do funkcji zwrotnej (zwykle typu void *). Jeśli używana biblioteka pozwala na ten dodatkowy argument, znajdziesz obejście tego problemu. W przeciwnym razie nie będziesz mieć możliwości osiągnięcia czysto tego, co chcesz zrobić.
Alexandre C.
Tak. Zdaję sobie sprawę, że interfejs API ftw.h i nftw.h jest wadliwy. Spróbuję fts.h
duncan
1
Świetny! /usr/include/fts.h:41:3: error: #error "<fts.h> nie może być używany z -D_FILE_OFFSET_BITS == 64"
duncan

Odpowiedzi:

48

Od przechwytywania lambdas trzeba zachować stan, nie jest naprawdę proste „obejście”, ponieważ są one nie tylko zwykłe funkcje. Punkt dotyczący wskaźnika funkcji polega na tym, że wskazuje on pojedynczą, globalną funkcję, a ta informacja nie ma miejsca na stan.

Najbliższym obejściem (które zasadniczo odrzuca stan) jest zapewnienie pewnego typu zmiennej globalnej, do której można uzyskać dostęp z poziomu lambda / funkcji. Na przykład, możesz stworzyć tradycyjny obiekt funktora i nadać mu statyczną funkcję składową, która odwołuje się do jakiejś unikalnej (globalnej / statycznej) instancji.

Ale to w pewnym sensie pokonuje cały cel przechwytywania lambd.

Kerrek SB
źródło
3
Bardziej przejrzystym rozwiązaniem jest umieszczenie lambdy wewnątrz adaptera przy założeniu, że wskaźnik funkcji ma parametr kontekstu.
Raymond Chen
4
@RaymondChen: Cóż, jeśli możesz określić, w jaki sposób funkcja ma być używana, to tak, to jest opcja. Chociaż w takim przypadku byłoby jeszcze łatwiej po prostu uczynić parametr argumentem samej lambdy!
Kerrek SB
3
@KerrekSB umieścił zmienne globalne w a namespacei oznacz je jako thread_local, to jest ftwpodejście, które wybrałem do rozwiązania czegoś podobnego.
Kjell Hedström
„wskaźnik funkcji wskazuje na pojedynczą, globalną funkcję, a ta informacja nie ma miejsca na stan”. -> Jak do cholery języki takie jak Java mogą to osiągnąć? Oczywiście, ponieważ ta pojedyncza, globalna funkcja jest tworzona w czasie wykonywania i osadza stan (a raczej odniesienie do niego) we własnym kodzie. Że jest cały punkt - nie powinno nie być pojedyncza, globalna funkcja ale wiele funkcji globalnych - po jednym dla każdego lambda jest używana w czasie wykonywania. Czy naprawdę w C ++ nie ma NIC, co to robi? (Myślałem, że std :: function jest stworzone dokładnie w tym jednym celu)
Dexter
1
@Dexter: errr .. krótka odpowiedź brzmi: nie, długa odpowiedź oznacza przeciążenie operatora. Niezależnie od tego, mój punkt widzenia się zgadza. Java to inny język, który nie jest tym samym, co C ++; Java nie ma wskaźników (ani przeciążalnych operatorów połączeń), a porównanie nie działa dobrze.
Kerrek SB
47

Właśnie napotkałem ten problem.

Kod kompiluje się dobrze bez przechwytywania lambda, ale podczas przechwytywania lambda występuje błąd konwersji typu.

Rozwiązaniem z C ++ 11 jest użycie std::function(edycja: inne rozwiązanie, które nie wymaga modyfikacji podpisu funkcji jest pokazane po tym przykładzie). Możesz także użyć boost::function(który faktycznie działa znacznie szybciej). Przykładowy kod - zmieniony tak, żeby się kompilował, skompilowany z 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;
}

Edycja: musiałem to ponownie odwiedzić, gdy natknąłem się na starszy kod, w którym nie mogłem zmodyfikować oryginalnej sygnatury funkcji, ale nadal potrzebowałem użyć lambd. Poniżej znajduje się rozwiązanie, które nie wymaga modyfikowania sygnatury funkcji oryginalnej funkcji:

#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
źródło
73
Nie, to nie powinna być akceptowana odpowiedź. Nie chodzi o to, ftwaby wziąć std::functionzamiast wskaźnika funkcji ...
Gregory Pakosz
Drugie rozwiązanie zaproponowane w tej odpowiedzi rozwiązuje obawy @ gregory-pakosz poprzez zachowanie oryginalnego podpisu, ale nadal nie jest świetne, ponieważ wprowadza stan globalny. Gdyby ftwmiał argument void * userdata, wolałbym odpowiedź z @ evgeny-karpov.
duma
@prideout zgodził się - ja też nie lubię stanu globalnego. Niestety, zakładając, że podpis ftw nie może być modyfikowany i biorąc pod uwagę, że nie ma on void * userdata, stan musi być gdzieś przechowywany. Napotkałem ten problem, korzystając z biblioteki innej firmy. Będzie to działać dobrze, o ile biblioteka nie przechwyci wywołania zwrotnego i nie użyje go później, w którym to przypadku zmienna globalna działa po prostu jak dodatkowy parametr na stosie wywołań. Jeśli podpis ftw można zmodyfikować, wolałbym użyć std :: function zamiast void * userdata.
Jay West,
1
jest to niezwykle skomplikowane i użyteczne rozwiązanie, @Gregory powinienem powiedzieć „to działa”.
fiorentinoing
17

ORYGINAŁ

Funkcje lambda są bardzo wygodne i redukują kod. W moim przypadku potrzebowałem lambd do programowania równoległego. Ale wymaga przechwytywania i wskaźników funkcji. Moje rozwiązanie jest tutaj. Uważaj jednak na zakres zmiennych, które przechwyciłeś.

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

Przykład

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

Przykład ze zwracaną wartością

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

AKTUALIZACJA

Poprawiona wersja

Minęło trochę czasu od opublikowania pierwszego postu o lambdzie w C ++ z przechwytywaniami jako wskaźnikiem funkcji. Ponieważ był użyteczny dla mnie i innych osób, dokonałem pewnej poprawy.

Standardowy interfejs API funkcji C używa konwencji void fn (void * data). Domyślnie używana jest ta konwencja, a lambda należy zadeklarować z argumentem void *.

Ulepszona implementacja

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

Konwersja lambda z przechwytywaniami do wskaźnika C.

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

Może być również używany w ten sposób

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

W przypadku, gdy należy użyć wartości zwracanej

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

I w przypadku wykorzystania danych

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
źródło
3
Jest to zdecydowanie najwygodniejsze rozwiązanie, jakie widziałem, aby przekonwertować lambdę na wskaźnik funkcji w stylu C. Funkcja przyjmująca go jako argument będzie potrzebować tylko dodatkowego parametru reprezentującego jej stan, często nazywanego „void * user” w bibliotekach C, aby mogła przekazać go do wskaźnika funkcji podczas jej wywoływania.
Codoscope
10

Używając lokalnie globalnej (statycznej) metody można to zrobić w następujący sposób

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

Załóżmy, że mamy

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

Więc użycie będzie

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

To działa, ponieważ każda lambda ma unikalną sygnaturę, więc uczynienie jej statyczną nie stanowi problemu. Poniżej znajduje się ogólne opakowanie z różną liczbą argumentów i dowolnym typem zwracanym przy użyciu tej samej metody.

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

I podobne użycie

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
źródło
1
pamiętaj, że to skopiuje zamknięcie (podczas pobierania ptr) + args (podczas wywoływania). W przeciwnym razie jest to eleganckie rozwiązanie
Ivan Sanz-Carasa
biblioteka pomocnicza tylko dla nagłówka: gist.github.com/isc30/fab67e5956fe8f2097bed84ebc42c1e8
Ivan Sanz-Carasa
1
@ IvanSanz-Carasa Dzięki za zwrócenie uwagi. Typy zamknięć nie są CopyAssignable, ale funktory. Więc masz rację, lepiej jest tutaj użyć idealnego przekazywania. Z drugiej strony dla argumentów nie możemy wiele zrobić, ponieważ zwykły C nie obsługuje uniwersalnych odwołań, ale przynajmniej możemy przekazać wartości z powrotem do naszej lambdy. Może to zaoszczędzić dodatkową kopię. Edytował kod.
Vladimir Talybin
@RiaD Tak, ponieważ lambda jest tutaj statyczną instancją, musisz zamiast tego przechwycić przez odniesienie, np. Zamiast =używać &iw pętli for.
Vladimir Talybin
5

Hehe - dość stare pytanie, ale wciąż ...

#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
źródło
0

Istnieje hakerski sposób na przekonwertowanie przechwytywanej lambdy na wskaźnik funkcji, ale podczas jej używania należy zachować ostrożność:

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

Twój kod wyglądałby wtedy tak (ostrzeżenie: kompilacja mózgu):

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
źródło
0

Moje rozwiązanie, po prostu użyj wskaźnika funkcji, aby odwołać się do statycznej lambdy.

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
źródło