C ++ lambda dengan captures sebagai penunjuk fungsi

94

Saya bermain dengan C ++ lambda dan konversi implisitnya ke fungsi pointer. Contoh awal saya menggunakannya sebagai callback untuk fungsi ftw. Ini bekerja seperti yang diharapkan.

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

Setelah mengubahnya untuk menggunakan tangkapan:

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

Saya mendapat kesalahan kompiler:

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)

Setelah membaca. Saya belajar bahwa lambda menggunakan penangkapan tidak dapat secara implisit dikonversi ke pointer fungsi.

Apakah ada solusi untuk ini? Apakah fakta bahwa mereka tidak dapat dikonversi "secara implisit" berarti bahwa mereka dapat "secara eksplisit" dikonversi? (Saya mencoba casting, tapi tidak berhasil). Apa cara yang bersih untuk memodifikasi contoh kerja sehingga saya bisa menambahkan entri ke beberapa objek menggunakan lambdas ?.

duncan
sumber
Kompiler apa yang Anda gunakan? apakah itu VS10?
Ramon Zarazua B.
gcc versi 4.6.1 20110801 [revisi gcc-4_6-branch 177033] (SUSE Linux)
duncan
4
Biasanya, cara C untuk meneruskan status ke callback dilakukan melalui argumen tambahan ke callback (biasanya tipe void *). Jika pustaka yang Anda gunakan memungkinkan untuk argumen tambahan ini, Anda akan menemukan solusinya. Jika tidak, Anda tidak bisa mencapai apa yang ingin Anda lakukan dengan bersih.
Alexandre C.
Iya. Saya menyadari bahwa api ftw.h dan nftw.h cacat. Saya akan mencoba fts.h
duncan
1
Bagus! /usr/include/fts.h:41:3: error: #error "<fts.h> tidak dapat digunakan dengan -D_FILE_OFFSET_BITS == 64"
duncan

Jawaban:

48

Karena menangkap lambda perlu mempertahankan status, sebenarnya tidak ada "solusi" sederhana, karena mereka bukan hanya fungsi biasa. Poin tentang penunjuk fungsi adalah ia menunjuk ke satu fungsi global, dan informasi ini tidak memiliki ruang untuk status.

Solusi terdekat (yang pada dasarnya membuang statefulness) adalah menyediakan beberapa jenis variabel global yang diakses dari lambda / function Anda. Misalnya, Anda dapat membuat objek functor tradisional dan memberinya fungsi anggota statis yang merujuk ke beberapa contoh unik (global / statis).

Tapi itu semacam mengalahkan seluruh tujuan menangkap lambda.

Kerrek SB
sumber
3
Solusi yang lebih bersih adalah membungkus lambda di dalam adaptor, dengan asumsi bahwa penunjuk fungsi memiliki parameter konteks.
Raymond Chen
4
@RaymondChen: Nah, jika Anda bebas menentukan bagaimana fungsi akan digunakan, maka ya, itu adalah opsi. Meskipun dalam hal ini akan lebih mudah untuk membuat parameter sebagai argumen dari lambda itu sendiri!
Kerrek SB
3
@KerrekSB meletakkan variabel global di a namespacedan menandainya sebagai thread_local, itulah ftwpendekatan yang saya pilih untuk menyelesaikan sesuatu yang serupa.
Kjell Hedström
"penunjuk fungsi menunjuk ke satu fungsi global, dan informasi ini tidak memiliki ruang untuk suatu keadaan." -> Bagaimana sih bahasa seperti Java bisa mencapai ini? Tentu saja, karena fungsi global tunggal itu dibuat saat runtime dan menyematkan status (atau lebih tepatnya referensi ke sana) dalam kodenya sendiri. Itu adalah seluruh titik - seharusnya tidak menjadi tunggal, fungsi global tetapi beberapa fungsi global - satu untuk setiap kali lambda digunakan dalam runtime. Apakah benar-benar TIDAK ADA di C ++ yang melakukan itu? (Saya pikir std :: function dibuat persis untuk tujuan tunggal itu)
Dexter
1
@Dexter: errr .. jawaban singkatnya tidak, jawaban panjang melibatkan operator yang kelebihan beban. Terlepas dari itu, maksud saya tetap. Java adalah bahasa berbeda yang tidak sama dengan C ++; Java tidak memiliki pointer (atau operator panggilan yang dapat kelebihan beban) dan perbandingannya tidak berfungsi dengan baik.
Kerrek SB
47

Saya baru saja mengalami masalah ini.

Kode dikompilasi dengan baik tanpa tangkapan lambda, tetapi ada kesalahan jenis konversi dengan tangkapan lambda.

Solusi dengan C ++ 11 adalah dengan menggunakan std::function(edit: solusi lain yang tidak memerlukan modifikasi tanda tangan fungsi akan ditampilkan setelah contoh ini). Anda juga dapat menggunakan boost::function(yang sebenarnya berjalan jauh lebih cepat). Contoh kode - diubah sehingga akan dikompilasi, dikompilasi dengan 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;
}

Sunting: Saya harus mengunjungi kembali ini ketika saya menemukan kode warisan di mana saya tidak dapat mengubah tanda tangan fungsi asli, tetapi masih perlu menggunakan lambda. Solusi yang tidak memerlukan modifikasi tanda tangan fungsi dari fungsi asli ada di bawah ini:

#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
sumber
73
Tidak, ini seharusnya bukan jawaban yang diterima. Intinya tidak berubah ftwmenjadi mengambil std::functionalih-alih penunjuk fungsi ...
Gregory Pakosz
Solusi kedua yang diusulkan dalam jawaban ini menjawab kekhawatiran dari @ gregory-pakosz dengan mempertahankan tanda tangan asli, tetapi tetap tidak bagus karena memperkenalkan status global. Jika ftwmemiliki argumen void * userdata, maka saya lebih suka jawaban dari @ evgeny-karpov.
Prideout
@prideout setuju - Saya juga tidak suka keadaan global. Sayangnya, dengan asumsi tanda tangan ftw tidak dapat dimodifikasi dan tidak memiliki void * userdata, status harus disimpan di suatu tempat. Saya mengalami masalah ini menggunakan pustaka pihak ketiga. Ini akan berfungsi dengan baik selama library tidak menangkap callback dan menggunakannya nanti, dalam hal ini variabel global hanya bertindak seperti parameter tambahan pada tumpukan panggilan. Jika tanda tangan ftw dapat dimodifikasi, maka saya lebih suka menggunakan std :: function daripada void * userdata.
Jay West
1
ini adalah solusi yang sangat rumit dan berguna, @Gregory Saya harus memberi tahu Anda "berhasil".
fiorentinoing
17

ASLI

Fungsi Lambda sangat nyaman dan mengurangi kode. Dalam kasus saya, saya membutuhkan lambda untuk pemrograman paralel. Tapi itu membutuhkan penunjuk menangkap dan fungsi. Solusi saya ada di sini. Tapi hati-hati dengan ruang lingkup variabel yang Anda tangkap.

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

Contoh

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

Contoh dengan nilai kembali

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

MEMPERBARUI

Versi yang ditingkatkan

Sudah lama sejak posting pertama tentang C ++ lambda dengan captures sebagai penunjuk fungsi telah diposting. Karena itu dapat digunakan untuk saya dan orang lain, saya membuat beberapa peningkatan.

Api pointer C fungsi standar menggunakan konvensi void fn (void * data). Secara default, konvensi ini digunakan dan lambda harus dideklarasikan dengan argumen void *.

Implementasi yang lebih baik

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

Mengonversi lambda dengan tangkapan ke penunjuk C.

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

Bisa digunakan dengan cara ini juga

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

Dalam kasus nilai kembali harus digunakan

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

Dan jika data digunakan

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
sumber
3
Ini jelas merupakan solusi paling nyaman yang pernah saya lihat untuk mengonversi lambda menjadi penunjuk fungsi gaya C. Fungsi yang menganggapnya sebagai argumen hanya akan membutuhkan parameter tambahan yang mewakili statusnya, sering kali dinamai "void * user" di pustaka C, sehingga dapat meneruskannya ke penunjuk fungsi saat memanggilnya.
Codoscope
10

Menggunakan metode global lokal (statis) dapat dilakukan sebagai berikut

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

Misalkan kita punya

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

Jadi penggunaannya akan

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

Ini berfungsi karena setiap lambda memiliki tanda tangan unik sehingga membuatnya statis tidak menjadi masalah. Berikut ini adalah pembungkus umum dengan jumlah argumen yang bervariasi dan tipe kembalian apa pun yang menggunakan metode yang sama.

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

Dan penggunaan serupa

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
sumber
1
Sadarilah bahwa ini akan menyalin closure (saat mendapatkan ptr) + args (saat memanggil). Jika tidak, ini adalah solusi yang elegan
Ivan Sanz-Carasa
perpustakaan pembantu header-only: gist.github.com/isc30/fab67e5956fe8f2097bed84ebc42c1e8
Ivan Sanz-Carasa
1
@ IvanSanz-Carasa Terima kasih telah menunjukkan. Jenis penutupan bukan CopyAssignable, tetapi fungsinya adalah. Jadi Anda benar, lebih baik gunakan penerusan sempurna di sini. Untuk args di sisi lain, kami tidak dapat melakukan banyak hal karena C biasa tidak mendukung referensi universal, tetapi setidaknya kami dapat meneruskan nilai kembali ke lambda kami. Ini dapat menghemat salinan tambahan. Memiliki kode yang diedit.
Vladimir Talybin
@RiaD Ya, karena lambda adalah instance statis di sini, Anda perlu menangkapnya dengan referensi, misalnya, alih-alih =digunakan &idalam loop-for Anda.
Vladimir Talybin
5

Hehe - pertanyaan yang cukup lama, tapi tetap saja ...

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

Ada cara hackish untuk mengonversi lambda penangkap menjadi penunjuk fungsi, tetapi Anda harus berhati-hati saat menggunakannya:

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

Kode Anda kemudian akan terlihat seperti ini (peringatan: kompilasi otak):

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;
}
pengguna1095108
sumber
0

Solusi saya, cukup gunakan penunjuk fungsi untuk merujuk ke lambda statis.

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
sumber