Melewati penangkapan lambda sebagai penunjuk fungsi

210

Apakah mungkin untuk melewatkan fungsi lambda sebagai penunjuk fungsi? Jika demikian, saya pasti melakukan sesuatu yang salah karena saya mendapatkan kesalahan kompilasi.

Perhatikan contoh berikut

using DecisionFn = bool(*)();

class Decide
{
public:
    Decide(DecisionFn dec) : _dec{dec} {}
private:
    DecisionFn _dec;
};

int main()
{
    int x = 5;
    Decide greaterThanThree{ [x](){ return x > 3; } };
    return 0;
}

Ketika saya mencoba mengkompilasi ini , saya mendapatkan kesalahan kompilasi berikut:

In function 'int main()':
17:31: error: the value of 'x' is not usable in a constant expression
16:9:  note: 'int x' is not const
17:53: error: no matching function for call to 'Decide::Decide(<brace-enclosed initializer list>)'
17:53: note: candidates are:
9:5:   note: Decide::Decide(DecisionFn)
9:5:   note: no known conversion for argument 1 from 'main()::<lambda()>' to 'DecisionFn {aka bool (*)()}'
6:7:   note: constexpr Decide::Decide(const Decide&)
6:7:   note: no known conversion for argument 1 from 'main()::<lambda()>' to 'const Decide&'
6:7:   note: constexpr Decide::Decide(Decide&&)
6:7:   note: no known conversion for argument 1 from 'main()::<lambda()>' to 'Decide&&'

Itu salah satu heck dari pesan kesalahan untuk dicerna, tapi saya pikir apa yang saya dapatkan adalah bahwa lambda tidak dapat diperlakukan sebagai constexprjadi karena itu saya tidak bisa meneruskannya sebagai penunjuk fungsi? Saya sudah mencoba membuat xconst juga, tapi itu sepertinya tidak membantu.

Cory Kramer
sumber
34
lambda dapat meluruh untuk berfungsi pointer hanya jika mereka tidak menangkap apa pun.
Jarod42
Sebagai anak cucu, posting blog yang ditautkan di atas sekarang tinggal di devblogs.microsoft.com/oldnewthing/20150220-00/?p=44623
warrenm

Jawaban:

205

Sebuah lambda hanya dapat dikonversi ke fungsi pointer jika tidak menangkap, dari rancangan C ++ 11 standar bagian 5.1.2 [expr.prim.lambda] mengatakan ( penekanan ):

Tipe penutup untuk ekspresi lambda tanpa tangkapan lambda memiliki fungsi konversi konstanta non-virtual publik non-virtual untuk mengarahkan ke fungsi yang memiliki parameter yang sama dan mengembalikan tipe seperti operator panggilan fungsi tipe penutupan. Nilai yang dikembalikan oleh fungsi konversi ini harus menjadi alamat fungsi yang, ketika dipanggil, memiliki efek yang sama dengan memanggil operator panggilan fungsi tipe penutupan.

Catatan, cppreferensi juga membahas hal ini di bagian mereka tentang fungsi Lambda .

Jadi, alternatif berikut bisa digunakan:

typedef bool(*DecisionFn)(int);

Decide greaterThanThree{ []( int x ){ return x > 3; } };

dan begitu juga ini:

typedef bool(*DecisionFn)();

Decide greaterThanThree{ [](){ return true ; } };

dan seperti yang ditunjukkan 5gon12eder , Anda juga dapat menggunakan std::function, tetapi perhatikan bahwa itu std::functionadalah beban yang berat , jadi itu bukan trade-off yang lebih murah.

Shafik Yaghmour
sumber
2
Catatan: Salah satu solusi umum yang digunakan oleh barang-barang C adalah untuk lulus void*sebagai parameter tunggal. Biasanya disebut "pointer pengguna". Ini relatif ringan juga, tetapi cenderung mengharuskan Anda mallockeluar beberapa ruang.
Dana Gugatan Monica
94

Jawaban Shafik Yaghmour dengan tepat menjelaskan mengapa lambda tidak dapat dilewatkan sebagai penunjuk fungsi jika memiliki tangkapan. Saya ingin menunjukkan dua perbaikan sederhana untuk masalah ini.

  1. Gunakan std::functionsebagai ganti pointer fungsi mentah.

    Ini adalah solusi yang sangat bersih. Namun perlu dicatat bahwa itu termasuk beberapa overhead tambahan untuk penghapusan tipe (mungkin panggilan fungsi virtual).

    #include <functional>
    #include <utility>
    
    struct Decide
    {
      using DecisionFn = std::function<bool()>;
      Decide(DecisionFn dec) : dec_ {std::move(dec)} {}
      DecisionFn dec_;
    };
    
    int
    main()
    {
      int x = 5;
      Decide greaterThanThree { [x](){ return x > 3; } };
    }
  2. Gunakan ekspresi lambda yang tidak menangkap apa pun.

    Karena predikat Anda benar-benar hanya konstanta boolean, berikut ini akan dengan cepat mengatasi masalah saat ini. Lihat jawaban ini untuk penjelasan yang baik mengapa dan bagaimana ini bekerja.

    // Your 'Decide' class as in your post.
    
    int
    main()
    {
      int x = 5;
      Decide greaterThanThree {
        (x > 3) ? [](){ return true; } : [](){ return false; }
      };
    }
5gon12eder
sumber
4
@TC. Lihat pertanyaan ini untuk perincian mengapa ini berhasil
Shafik Yaghmour
Perhatikan bahwa secara umum, jika Anda mengetahui data penangkapan pada waktu kompilasi, Anda dapat mengonversinya untuk mengetik data dan kemudian Anda kembali memiliki lambda tanpa tangkapan - lihat jawaban ini yang baru saja saya tulis untuk pertanyaan lain (terima kasih kepada @ 5gon12eder jawab di sini).
dan-man
Bukankah seharusnya objek memiliki umur yang lebih panjang dari fungsi pointer? Saya ingin menggunakannya untuk glutReshapeFunc.
ar2015
saya tidak merekomendasikan saran ini, hal-hal yang cenderung bekerja secara ajaib, memperkenalkan kesalahan baru. dan praktik yang sejalan dengan kesalahan tersebut. jika Anda ingin menggunakan fungsi std ::, Anda akan melihat semua jenis cara yang bisa digunakan fungsi std ::. karena beberapa cara mungkin sesuatu yang tidak Anda inginkan.
TheNegative
1
Ini tidak menjawab pertanyaan. Jika seseorang bisa menggunakan std::functionatau lambda - mengapa tidak? Paling tidak itu adalah sintaks yang lebih mudah dibaca. Biasanya kita perlu menggunakan pointer fungsi untuk berinteraksi dengan pustaka C (sebenarnya, dengan pustaka eksternal apa pun) , dan pastikan Anda tidak dapat memodifikasinya untuk menerima std :: function atau lambda.
Hi-Angel
40

Ekspresi Lambda, bahkan yang ditangkap, dapat ditangani sebagai penunjuk fungsi (penunjuk ke fungsi anggota).

Ini rumit karena ekspresi lambda bukanlah fungsi yang sederhana. Ini sebenarnya adalah objek dengan operator ().

Ketika Anda kreatif, Anda dapat menggunakan ini! Pikirkan kelas "function" dengan gaya std :: function. Jika Anda menyimpan objek, Anda juga dapat menggunakan penunjuk fungsi.

Untuk menggunakan pointer fungsi, Anda dapat menggunakan yang berikut:

int first = 5;
auto lambda = [=](int x, int z) {
    return x + z + first;
};
int(decltype(lambda)::*ptr)(int, int)const = &decltype(lambda)::operator();
std::cout << "test = " << (lambda.*ptr)(2, 3) << std::endl;

Untuk membangun kelas yang dapat mulai bekerja seperti "std :: function", pertama-tama Anda membutuhkan kelas / struct daripada yang dapat menyimpan objek dan fungsi pointer. Anda juga memerlukan operator () untuk menjalankannya:

// OT => Object Type
// RT => Return Type
// A ... => Arguments
template<typename OT, typename RT, typename ... A>
struct lambda_expression {
    OT _object;
    RT(OT::*_function)(A...)const;

    lambda_expression(const OT & object)
        : _object(object), _function(&decltype(_object)::operator()) {}

    RT operator() (A ... args) const {
        return (_object.*_function)(args...);
    }
};

Dengan ini, Anda sekarang dapat menjalankan lambdas yang ditangkap dan tidak ditangkap, seperti halnya Anda menggunakan yang asli:

auto capture_lambda() {
    int first = 5;
    auto lambda = [=](int x, int z) {
        return x + z + first;
    };
    return lambda_expression<decltype(lambda), int, int, int>(lambda);
}

auto noncapture_lambda() {
    auto lambda = [](int x, int z) {
        return x + z;
    };
    return lambda_expression<decltype(lambda), int, int, int>(lambda);
}

void refcapture_lambda() {
    int test;
    auto lambda = [&](int x, int z) {
        test = x + z;
    };
    lambda_expression<decltype(lambda), void, int, int>f(lambda);
    f(2, 3);

    std::cout << "test value = " << test << std::endl;
}

int main(int argc, char **argv) {
    auto f_capture = capture_lambda();
    auto f_noncapture = noncapture_lambda();

    std::cout << "main test = " << f_capture(2, 3) << std::endl;
    std::cout << "main test = " << f_noncapture(2, 3) << std::endl;

    refcapture_lambda();

    system("PAUSE");
    return 0;
}

Kode ini bekerja dengan VS2015

Pembaruan 04.07.17:

template <typename CT, typename ... A> struct function
: public function<decltype(&CT::operator())(A...)> {};

template <typename C> struct function<C> {
private:
    C mObject;

public:
    function(const C & obj)
        : mObject(obj) {}

    template<typename... Args> typename 
    std::result_of<C(Args...)>::type operator()(Args... a) {
        return this->mObject.operator()(a...);
    }

    template<typename... Args> typename 
    std::result_of<const C(Args...)>::type operator()(Args... a) const {
        return this->mObject.operator()(a...);
    }
};

namespace make {
    template<typename C> auto function(const C & obj) {
        return ::function<C>(obj);
    }
}

int main(int argc, char ** argv) {
   auto func = make::function([](int y, int x) { return x*y; });
   std::cout << func(2, 4) << std::endl;
   system("PAUSE");
   return 0;
}
Noxxer
sumber
Wow Luar biasa! Jadi kita bisa menggunakan pointer dalam kelas lambda (ke operator fungsi anggota ()) untuk memanggil lambda yang tersimpan di kelas pembungkus !! LUAR BIASA !! Mengapa kita membutuhkan fungsi std ::? Dan mungkinkah membuat lambda_expression <decltype (lambda), int, int, int> untuk secara otomatis menyimpulkan / parameter "int" ini langsung dari lambda yang disahkan itu sendiri?
barney
2
Saya telah menambahkan versi pendek dari kode saya sendiri. ini seharusnya bekerja dengan auto sederhana f = make :: function (lambda); Tapi saya cukup yakin Anda akan menemukan banyak situasi kode saya tidak akan berfungsi. std :: function jauh lebih baik dibangun daripada ini dan harus digunakan ketika Anda sedang bekerja. Ini di sini untuk pendidikan dan penggunaan pribadi.
Noxxer
14
Solusi ini melibatkan memanggil lambda melalui operator()implementasi, jadi jika saya membacanya dengan benar, saya pikir itu tidak akan berhasil untuk memanggil lambda menggunakan pointer fungsi C-style , bukan? Itulah pertanyaan awal yang ditanyakan.
Remy Lebeau
13
Anda mengklaim bahwa lambdas dapat ditangani sebagai pointer fungsi, yang tidak Anda lakukan. Anda membuat objek lain untuk memegang lambda, yang tidak melakukan apa-apa, Anda bisa saja menggunakan lambda asli.
Passer By
9
Ini bukan "meneruskan menangkap lambda sebagai penunjuk fungsi". Ini adalah "melewati menangkap lambda sebagai objek yang berisi pointer fungsi antara lain". Ada dunia perbedaan.
n. 'kata ganti' m.
15

Menangkap lambda tidak dapat dikonversi menjadi pointer fungsi, seperti yang ditunjukkan oleh jawaban ini .

Namun, seringkali cukup merepotkan untuk menyediakan pointer fungsi ke API yang hanya menerima satu. Metode yang paling sering dikutip untuk melakukannya adalah menyediakan fungsi dan memanggil objek statis dengannya.

static Callable callable;
static bool wrapper()
{
    return callable();
}

Ini membosankan. Kami mengambil ide ini lebih jauh dan mengotomatiskan proses menciptakan wrapperdan membuat hidup lebih mudah.

#include<type_traits>
#include<utility>

template<typename Callable>
union storage
{
    storage() {}
    std::decay_t<Callable> callable;
};

template<int, typename Callable, typename Ret, typename... Args>
auto fnptr_(Callable&& c, Ret (*)(Args...))
{
    static bool used = false;
    static storage<Callable> s;
    using type = decltype(s.callable);

    if(used)
        s.callable.~type();
    new (&s.callable) type(std::forward<Callable>(c));
    used = true;

    return [](Args... args) -> Ret {
        return Ret(s.callable(std::forward<Args>(args)...));
    };
}

template<typename Fn, int N = 0, typename Callable>
Fn* fnptr(Callable&& c)
{
    return fnptr_<N>(std::forward<Callable>(c), (Fn*)nullptr);
}

Dan gunakan sebagai

void foo(void (*fn)())
{
    fn();   
}

int main()
{
    int i = 42;
    auto fn = fnptr<void()>([i]{std::cout << i;});
    foo(fn);  // compiles!
}

Hidup

Ini pada dasarnya mendeklarasikan fungsi anonim pada setiap kemunculan fnptr.

Perhatikan bahwa doa fnptrmenimpa callablecallable yang diberikan sebelumnya ditulis dari jenis yang sama. Kami memperbaiki ini, pada tingkat tertentu, dengan intparameter N.

std::function<void()> func1, func2;
auto fn1 = fnptr<void(), 1>(func1);
auto fn2 = fnptr<void(), 2>(func2);  // different function
Melewati
sumber
memaksa integer N untuk dideklarasikan akan menjadi cara yang elegan untuk mengingat klien untuk menghindari menimpa fungsi pointer pada waktu kompilasi.
fiorentinoing
2

Pintasan untuk menggunakan lambda dengan sebagai pointer fungsi C adalah ini:

"auto fun = +[](){}"

Menggunakan Curl sebagai contoh ( info debug keriting )

auto callback = +[](CURL* handle, curl_infotype type, char* data, size_t size, void*){ //add code here :-) };
curl_easy_setopt(curlHande, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(curlHande,CURLOPT_DEBUGFUNCTION,callback);
janCoffee
sumber
3
Lambda itu tidak memiliki tangkapan. Masalah OP adalah penangkapan, tidak harus menyimpulkan tipe pointer fungsi (yang merupakan +triknya untuk Anda).
Sneftel
2

Meskipun pendekatan templat cerdas karena berbagai alasan, penting untuk mengingat siklus hidup lambda dan variabel yang ditangkap. Jika segala bentuk penunjuk lambda akan digunakan dan lambda bukan merupakan kelanjutan ke bawah, maka hanya salinan [=] lambda yang harus digunakan. Yaitu, bahkan kemudian, menangkap pointer ke variabel pada stack tidak aman jika masa pakai pointer yang ditangkap (stack relax) lebih pendek dari masa pakai lambda.

Solusi sederhana untuk menangkap lambda sebagai pointer adalah:

auto pLamdba = new std::function<...fn-sig...>([=](...fn-sig...){...});

misalnya, new std::function<void()>([=]() -> void {...}

Ingat saja nanti, delete pLamdbajadi pastikan Anda tidak membocorkan memori lambda. Rahasia yang harus disadari di sini adalah bahwa lambda dapat menangkap lambdas (tanyakan pada diri Anda bagaimana cara kerjanya) dan juga agar agar std::functiondapat bekerja secara umum, implementasi lambda perlu mengandung informasi internal yang memadai untuk menyediakan akses ke ukuran data lambda (dan yang ditangkap) ( itulah sebabnya mengapa deleteharus bekerja [menjalankan destruktor dari tipe yang ditangkap]).

skrip kecil
sumber
Mengapa repot dengan new- std :: function sudah menyimpan lambda di heap AND dan tidak perlu mengingat panggilan delete.
Chris Dodd
0

Bukan jawaban langsung, tetapi sedikit variasi untuk menggunakan pola templat "functor" untuk menyembunyikan spesifikasi jenis lambda dan menjaga kode tetap bagus dan sederhana.

Saya tidak yakin bagaimana Anda ingin menggunakan kelas putuskan jadi saya harus memperluas kelas dengan fungsi yang menggunakannya. Lihat contoh lengkap di sini: https://godbolt.org/z/jtByqE

Bentuk dasar kelas Anda mungkin terlihat seperti ini:

template <typename Functor>
class Decide
{
public:
    Decide(Functor dec) : _dec{dec} {}
private:
    Functor _dec;
};

Di mana Anda melewati tipe fungsi sebagai bagian dari tipe kelas yang digunakan seperti:

auto decide_fc = [](int x){ return x > 3; };
Decide<decltype(decide_fc)> greaterThanThree{decide_fc};

Sekali lagi, saya tidak yakin mengapa Anda menangkapnya xlebih masuk akal (bagi saya) untuk memiliki parameter yang Anda berikan ke lambda) sehingga Anda dapat menggunakan seperti:

int result = _dec(5); // or whatever value

Lihat tautan untuk contoh lengkap

code_fodder
sumber
-2

Seperti yang disebutkan oleh yang lain, Anda dapat mengganti fungsi Lambda alih-alih fungsi pointer. Saya menggunakan metode ini di antarmuka C ++ saya ke F77 ODE RKSUITE pemecah.

//C interface to Fortran subroutine UT
extern "C"  void UT(void(*)(double*,double*,double*),double*,double*,double*,
double*,double*,double*,int*);

// C++ wrapper which calls extern "C" void UT routine
static  void   rk_ut(void(*)(double*,double*,double*),double*,double*,double*,
double*,double*,double*,int*);

//  Call of rk_ut with lambda passed instead of function pointer to derivative
//  routine
mathlib::RungeKuttaSolver::rk_ut([](double* T,double* Y,double* YP)->void{YP[0]=Y[1]; YP[1]= -Y[0];}, TWANT,T,Y,YP,YMAX,WORK,UFLAG);
beniekg
sumber