Pertanyaan Apa ekspresi lambda dalam C ++ 11?


Apa ekspresi lambda dalam C ++ 11? Kapan saya akan menggunakannya? Apa kelas masalah yang mereka selesaikan yang tidak mungkin sebelum pengenalan mereka?

Beberapa contoh, dan menggunakan kasus akan berguna.


1195
2017-10-02 14:58


asal


Jawaban:


Masalah

C ++ termasuk fungsi generik yang berguna seperti std::for_each dan std::transform, yang bisa sangat berguna. Sayangnya mereka juga bisa sangat tidak praktis untuk digunakan, terutama jika functor yang ingin Anda terapkan adalah unik untuk fungsi tertentu.

#include <algorithm>
#include <vector>

namespace {
  struct f {
    void operator()(int) {
      // do something
    }
  };
}

void func(std::vector<int>& v) {
  f f;
  std::for_each(v.begin(), v.end(), f);
}

Jika Anda hanya menggunakan f satu kali dan di tempat tertentu itu tampaknya berlebihan untuk menulis seluruh kelas hanya untuk melakukan sesuatu yang sepele dan satu lagi.

Dalam C ++ 03 Anda mungkin tergoda untuk menulis sesuatu seperti berikut, untuk menjaga functor lokal:

void func2(std::vector<int>& v) {
  struct {
    void operator()(int) {
       // do something
    }
  } f;
  std::for_each(v.begin(), v.end(), f);
}

Namun ini tidak diperbolehkan, f tidak bisa dilewatkan ke fungsi template di C ++ 03.

Solusi baru

C ++ 11 memperkenalkan lambdas memungkinkan Anda untuk menulis inline, fungsi anonim untuk menggantikan struct f. Untuk contoh sederhana kecil ini bisa lebih bersih untuk dibaca (membuat semuanya di satu tempat) dan berpotensi lebih sederhana untuk dipertahankan, misalnya dalam bentuk yang paling sederhana:

void func3(std::vector<int>& v) {
  std::for_each(v.begin(), v.end(), [](int) { /* do something here*/ });
}

Fungsi Lambda hanyalah gula sintaksis untuk fungsi anonim.

Kembalikan jenis

Dalam kasus sederhana, jenis kembalinya lambda dideduksi untuk Anda, misalnya:

void func4(std::vector<double>& v) {
  std::transform(v.begin(), v.end(), v.begin(),
                 [](double d) { return d < 0.00001 ? 0 : d; }
                 );
}

namun ketika Anda mulai menulis lambdas yang lebih kompleks Anda akan segera menemukan kasus-kasus di mana jenis kembalinya tidak dapat dideduksi oleh kompilator, misalnya:

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

Untuk mengatasi ini Anda diizinkan secara eksplisit menentukan jenis kembalian untuk fungsi lambda, menggunakan -> T:

void func4(std::vector<double>& v) {
    std::transform(v.begin(), v.end(), v.begin(),
        [](double d) -> double {
            if (d < 0.0001) {
                return 0;
            } else {
                return d;
            }
        });
}

Variabel "Menangkap"

Sejauh ini kita tidak menggunakan apa pun selain apa yang diteruskan ke lambda di dalamnya, tetapi kita juga dapat menggunakan variabel lain, di dalam lambda. Jika Anda ingin mengakses variabel lain, Anda dapat menggunakan klausa pengambilan ( [] dari ekspresi), yang sejauh ini belum digunakan dalam contoh-contoh ini, misalnya:

void func5(std::vector<double>& v, const double& epsilon) {
    std::transform(v.begin(), v.end(), v.begin(),
        [epsilon](double d) -> double {
            if (d < epsilon) {
                return 0;
            } else {
                return d;
            }
        });
}

Anda dapat menangkap dengan referensi dan nilai, yang dapat Anda tentukan menggunakan & dan = masing-masing:

  • [&epsilon] tangkap dengan referensi
  • [&] menangkap semua variabel yang digunakan dalam lambda dengan referensi
  • [=] menangkap semua variabel yang digunakan dalam lambda berdasarkan nilai
  • [&, epsilon] menangkap variabel seperti dengan [&], tetapi epsilon berdasarkan nilai
  • [=, &epsilon] menangkap variabel seperti dengan [=], tetapi epsilon dengan referensi

Yang dihasilkan operator() aku s const secara default, dengan implikasi yang akan ditangkap const saat Anda mengaksesnya secara default. Ini memiliki efek bahwa setiap panggilan dengan input yang sama akan menghasilkan hasil yang sama, namun Anda bisa tandai lambda sebagai mutable untuk meminta itu operator() yang dihasilkan tidak const.


1206
2017-10-02 15:21



Apa fungsi lambda?

Konsep C ++ dari fungsi lambda berasal dari kalkulus lambda dan pemrograman fungsional. Lambda adalah fungsi yang tidak disebutkan namanya yang berguna (dalam pemrograman yang sebenarnya, bukan teori) untuk potongan pendek kode yang tidak mungkin digunakan kembali dan tidak layak disebut.

Dalam C ++ fungsi lambda didefinisikan seperti ini

[]() { } // barebone lambda

atau dalam segala kemegahannya

[]() mutable -> T { } // T is the return type, still lacking throw()

[] adalah daftar tangkap, () daftar argumen dan {} tubuh fungsi.

Daftar tangkap

Daftar tangkap mendefinisikan apa dari luar lambda harus tersedia di dalam tubuh fungsi dan bagaimana. Ini bisa berupa:

  1. nilai: [x]
  2. referensi [& x]
  3. variabel apa pun yang saat ini dalam cakupan berdasarkan referensi [&]
  4. sama dengan 3, tetapi berdasarkan nilai [=]

Anda dapat mencampur semua hal di atas dalam daftar yang dipisahkan koma [x, &y].

Daftar argumen

Daftar argumen sama dengan fungsi C ++ lainnya.

Tubuh fungsi

Kode yang akan dieksekusi ketika lambda benar-benar dipanggil.

Pengurangan jenis pengembalian

Jika lambda hanya memiliki satu pernyataan pengembalian, jenis kembalinya dapat dihilangkan dan memiliki tipe implisit decltype(return_statement).

Yg mungkin berubah

Jika lambda ditandai dapat berubah (mis. []() mutable { }) diperbolehkan untuk mengubah nilai yang telah ditangkap oleh nilai.

Gunakan kasus

Perpustakaan yang didefinisikan oleh standar ISO sangat diuntungkan oleh lambdas dan meningkatkan kegunaan beberapa bar karena sekarang pengguna tidak perlu mengacaukan kode mereka dengan fungsi kecil dalam beberapa lingkup yang dapat diakses.

C ++ 14

Dalam C ++ 14 lambdas telah diperpanjang oleh berbagai proposal.

Inisialisasi Lambda Captures

Unsur dari daftar pengambilan sekarang dapat diinisialisasi dengan =. Ini memungkinkan penggantian nama variabel dan untuk menangkap dengan bergerak. Contoh yang diambil dari standar:

int x = 4;
auto y = [&r = x, x = x+1]()->int {
            r += 2;
            return x+2;
         }();  // Updates ::x to 6, and initializes y to 7.

dan satu yang diambil dari Wikipedia menunjukkan cara menangkap dengan std::move:

auto ptr = std::make_unique<int>(10); // See below for std::make_unique
auto lambda = [ptr = std::move(ptr)] {return *ptr;};

Lambdas Generik

Lambdas sekarang bisa generik (auto akan setara dengan T di sini jika T adalah argumen tipe template di suatu tempat di lingkup sekitarnya):

auto lambda = [](auto x, auto y) {return x + y;};

Peningkatan Pengurangan Jenis Pengembalian

C + + 14 memungkinkan jenis pengembalian yang disimpulkan untuk setiap fungsi dan tidak membatasinya ke fungsi-fungsi bentuk return expression;. Ini juga diperluas ke lambdas.


718
2017-10-02 15:43



Ekspresi Lambda biasanya digunakan untuk mengenkapsulasi algoritme sehingga dapat diteruskan ke fungsi lain. Namun, adalah mungkin untuk mengeksekusi lambda segera setelah definisi:

[&](){ ...your code... }(); // immediately executed lambda expression

secara fungsional setara dengan

{ ...your code... } // simple code block

Ini membuat ekspresi lambda alat yang kuat untuk refactoring fungsi-fungsi kompleks. Anda mulai dengan membungkus bagian kode dalam fungsi lambda seperti yang ditunjukkan di atas. Proses parameterisasi eksplisit kemudian dapat dilakukan secara bertahap dengan pengujian lanjutan setelah setiap langkah. Setelah Anda memiliki blok kode sepenuhnya parameter (seperti yang ditunjukkan oleh penghapusan &), Anda dapat memindahkan kode ke lokasi eksternal dan menjadikannya fungsi normal.

Demikian pula, Anda dapat menggunakan ekspresi lambda untuk menginisialisasi variabel berdasarkan hasil dari suatu algoritma...

int a = []( int b ){ int r=1; while (b>0) r*=b--; return r; }(5); // 5!

Sebagai cara mempartisi logika program Anda, Anda bahkan mungkin merasa berguna untuk menyampaikan ekspresi lambda sebagai argumen ekspresi lambda lain ...

[&]( std::function<void()> algorithm ) // wrapper section
   {
   ...your wrapper code...
   algorithm();
   ...your wrapper code...
   }
([&]() // algorithm section
   {
   ...your algorithm code...
   });

Ekspresi Lambda juga memungkinkan Anda membuat nama fungsi bertingkat, yang bisa menjadi cara mudah untuk menghindari logika duplikat. Menggunakan lambdas bernama juga cenderung menjadi sedikit lebih mudah pada mata (dibandingkan dengan lambdas inline anonim) ketika melewati fungsi non-trivial sebagai parameter ke fungsi lain. Catatan: jangan lupa tanda titik koma setelah penutupan kurung kurawal.

auto algorithm = [&]( double x, double m, double b ) -> double
   {
   return m*x+b;
   };

int a=algorithm(1,2,3), b=algorithm(4,5,6);

Jika profil berikutnya mengungkapkan overhead inisialisasi yang signifikan untuk objek fungsi, Anda dapat memilih untuk menulis ulang ini sebagai fungsi normal.


151
2018-03-01 08:08



Jawaban

T: Apa ekspresi lambda dalam C ++ 11?

A: Di bawah tenda, itu adalah objek dari kelas autogenerated dengan overloading operator () const. Objek tersebut disebut penutupan dan dibuat oleh compiler. Konsep 'penutupan' ini dekat dengan konsep bind dari C ++ 11. Tapi lambdas biasanya menghasilkan kode yang lebih baik. Dan panggilan melalui penutupan memungkinkan penyisipan penuh.

T: Kapan saya akan menggunakannya?

A: Untuk mendefinisikan "logika sederhana dan kecil" dan meminta kompiler melakukan generasi dari pertanyaan sebelumnya. Anda memberikan beberapa ekspresi kompilator yang Anda ingin berada di dalam operator (). Semua kompiler barang lainnya akan menghasilkan untuk Anda.

T: Apa masalah kelas yang mereka selesaikan yang tidak mungkin sebelum pengenalan mereka?

J: Ini adalah sejenis gula sintaks seperti operator yang overload bukan fungsi untuk custom tambahkan, subrtact operasi ... Tapi itu menyimpan lebih banyak baris kode yang tidak diperlukan untuk membungkus 1-3 baris logika nyata ke beberapa kelas, dan lain-lain! Beberapa insinyur berpikir bahwa jika jumlah garis lebih kecil maka ada sedikit kesempatan untuk membuat kesalahan di dalamnya (saya juga berpikir demikian)

Contoh penggunaan

auto x = [=](int arg1){printf("%i", arg1); };
void(*f)(int) = x;
f(1);
x(1);

Ekstra tentang lambdas, tidak tercakup oleh pertanyaan. Abaikan bagian ini jika Anda tidak tertarik

1. Nilai yang diambil. Apa yang bisa Anda ambil

1.1. Anda dapat merujuk ke variabel dengan durasi penyimpanan statis di lambdas. Mereka semua ditangkap.

1.2. Anda dapat menggunakan lambda untuk nilai tangkapan "berdasarkan nilai". Dalam hal ini, var yang ditangkap akan disalin ke objek fungsi (penutupan).

[captureVar1,captureVar2](int arg1){}

1.3. Anda dapat menangkap menjadi referensi. & - dalam konteks ini berarti referensi, bukan pointer.

   [&captureVar1,&captureVar2](int arg1){}

1.4. Ini ada notasi untuk menangkap semua var non-statis berdasarkan nilai, atau dengan referensi

  [=](int arg1){} // capture all not-static vars by value

  [&](int arg1){} // capture all not-static vars by reference

1.5. Ini ada notasi untuk menangkap semua non-statis vars berdasarkan nilai, atau dengan referensi dan tentukan smth. lebih. Contoh: Tangkap semua vars tidak-statis berdasarkan nilai, tetapi dengan referensi capture Param2

[=,&Param2](int arg1){} 

Tangkap semua vars tidak-statis dengan referensi, tetapi dengan nilai capture Param2

[&,Param2](int arg1){} 

2. Pengurangan tipe pengembalian

2.1. Lambda return type dapat disimpulkan jika lambda adalah salah satu ekspresi. Atau Anda dapat secara eksplisit menentukannya.

[=](int arg1)->trailing_return_type{return trailing_return_type();}

Jika lambda memiliki lebih dari satu ekspresi, maka kembali jenis harus ditentukan melalui trailing return type.   Juga, sintaks yang serupa dapat diterapkan untuk fungsi-fungsi otomatis dan fungsi-anggota

3. Nilai yang diambil. Apa yang tidak bisa Anda tangkap

3.1. Anda hanya dapat menangkap vars lokal, bukan variabel anggota objek.

4. Сonversions

4.1. lambda bukan fungsi pointer dan itu bukan fungsi anonim, tetapi dapat dikonversi secara implisit ke penunjuk fungsi.

p.s. 

  1. Lebih lanjut tentang informasi grammar lambda dapat ditemukan di draf Kerja untuk Bahasa Pemrograman C ++ # 337, 2012-01-16, 5.1.2. Lambda Expressions, hal.88

  2. Dalam C ++ 14, fitur tambahan yang dinamai "init capture" telah ditambahkan. Ini memungkinkan untuk melakukan pernyataan anggota data penutupan secara militer:

    auto toFloat = [](int value) { return float(value);};
    auto interpolate = [min = toFloat(0), max = toFloat(255)](int value)->float { return (value - min) / (max - min);};
    

30
2018-06-03 16:40



Fungsi lambda adalah fungsi anonim yang Anda buat secara in-line. Ia dapat menangkap variabel seperti yang telah dijelaskan beberapa orang, (mis. http://www.stroustrup.com/C++11FAQ.html#lambda) tetapi ada beberapa batasan. Misalnya, jika ada antarmuka panggilan balik seperti ini,

void apply(void (*f)(int)) {
    f(10);
    f(20);
    f(30);
}

Anda dapat menulis fungsi di tempat untuk menggunakannya seperti yang diteruskan untuk diterapkan di bawah ini:

int col=0;
void output() {
    apply([](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

Tetapi Anda tidak dapat melakukan ini:

void output(int n) {
    int col=0;
    apply([&col,n](int data) {
        cout << data << ((++col % 10) ? ' ' : '\n');
    });
}

karena keterbatasan dalam standar C ++ 11. Jika Anda ingin menggunakan jepretan, Anda harus bergantung pada pustaka dan

#include <functional> 

(Atau beberapa pustaka STL lain seperti algoritme untuk mendapatkannya secara tidak langsung) dan kemudian bekerja dengan std :: function daripada meneruskan fungsi normal sebagai parameter seperti ini:

#include <functional>
void apply(std::function<void(int)> f) {
    f(10);
    f(20);
    f(30);
}
void output(int width) {
    int col;
    apply([width,&col](int data) {
        cout << data << ((++col % width) ? ' ' : '\n');
    });
}

12
2018-03-10 22:36



Salah satu penjelasan terbaik tentang lambda expression diberikan dari penulis C ++ Bjarne Stroustrup dalam bukunya ***The C++ Programming Language*** bab 11 (ISBN-13: 978-0321563842):

What is a lambda expression? 

SEBUAH ekspresi lambda, terkadang juga disebut sebagai lambda   berfungsi atau (secara tegas salah, tapi bahasa sehari-hari) sebagai    lambda, adalah notasi yang disederhanakan untuk mendefinisikan dan menggunakan objek fungsi anonim. Daripada mendefinisikan kelas bernama dengan operator (), kemudian membuat objek dari kelas itu, dan akhirnya   memohonnya, kita bisa menggunakan singkatan.

When would I use one?

Ini sangat berguna ketika kita ingin melewati operasi sebagai   argumen untuk suatu algoritma. Dalam konteks antarmuka pengguna grafis   (dan di tempat lain), operasi semacam itu sering disebut sebagai panggilan balik.

What class of problem do they solve that wasn't possible prior to their introduction?

Di sini saya kira setiap tindakan yang dilakukan dengan ekspresi lambda dapat diselesaikan tanpa mereka, tetapi dengan lebih banyak kode dan kompleksitas yang jauh lebih besar. Ekspresi Lambda ini adalah cara optimasi untuk kode Anda dan cara membuatnya lebih menarik. Sedih oleh Stroustup:

cara efektif untuk mengoptimalkan

Some examples

melalui ekspresi lambda

void print_modulo(const vector<int>& v, ostream& os, int m) // output v[i] to os if v[i]%m==0
{
    for_each(begin(v),end(v),
        [&os,m](int x) { 
           if (x%m==0) os << x << '\n';
         });
}

atau melalui fungsi

class Modulo_print {
         ostream& os; // members to hold the capture list int m;
     public:
         Modulo_print(ostream& s, int mm) :os(s), m(mm) {} 
         void operator()(int x) const
           { 
             if (x%m==0) os << x << '\n'; 
           }
};

atau bahkan

void print_modulo(const vector<int>& v, ostream& os, int m) 
     // output v[i] to os if v[i]%m==0
{
    class Modulo_print {
        ostream& os; // members to hold the capture list
        int m; 
        public:
           Modulo_print (ostream& s, int mm) :os(s), m(mm) {}
           void operator()(int x) const
           { 
               if (x%m==0) os << x << '\n';
           }
     };
     for_each(begin(v),end(v),Modulo_print{os,m}); 
}

jika kamu butuh kamu dapat nama lambda expression seperti di bawah ini:

void print_modulo(const vector<int>& v, ostream& os, int m)
    // output v[i] to os if v[i]%m==0
{
      auto Modulo_print = [&os,m] (int x) { if (x%m==0) os << x << '\n'; };
      for_each(begin(v),end(v),Modulo_print);
 }

Atau ambil contoh sederhana lainnya

void TestFunctions::simpleLambda() {
    bool sensitive = true;
    std::vector<int> v = std::vector<int>({1,33,3,4,5,6,7});

    sort(v.begin(),v.end(),
         [sensitive](int x, int y) {
             printf("\n%i\n",  x < y);
             return sensitive ? x < y : abs(x) < abs(y);
         });


    printf("sorted");
    for_each(v.begin(), v.end(),
             [](int x) {
                 printf("x - %i;", x);
             }
             );
}

akan menghasilkan selanjutnya

0

1

0

1

0

1

0

1

0

1

0 sortx - 1; x - 3; x - 4; x - 5; x - 6; x - 7; x - 33;

[] - ini adalah daftar pengambilan atau lambda introducer: jika lambdas tidak memerlukan akses ke lingkungan lokal mereka, kami dapat menggunakannya.

Kutipan dari buku:

Karakter pertama dari ekspresi lambda selalu [. Lambda   introducer dapat mengambil berbagai bentuk:

[]: daftar pengambilan yang kosong. Ini   menyiratkan bahwa tidak ada nama lokal dari konteks sekitarnya yang dapat digunakan   di tubuh lambda. Untuk ekspresi lambda seperti itu, data diperoleh dari   argumen atau dari variabel nonlocal.

[&]: ditangkap secara implisit oleh   referensi. Semua nama lokal dapat digunakan. Semua variabel lokal adalah   diakses oleh referensi.

[=]: secara implisit menangkap berdasarkan nilai. Semua lokal   nama dapat digunakan. Semua nama mengacu pada salinan variabel lokal   diambil pada titik panggilan ekspresi lambda.

[daftar tangkap]:  penangkapan eksplisit; capture-list adalah daftar nama-nama variabel lokal yang akan diambil (yaitu, disimpan dalam objek) oleh referensi atau berdasarkan nilai. Variabel dengan nama yang didahului oleh & ditangkap oleh   referensi. Variabel lain ditangkap oleh nilai. Daftar tangkap bisa   juga mengandung ini dan nama-nama yang diikuti oleh ... sebagai elemen.

[&, daftar tangkap]: menangkap secara implisit dengan merujuk semua variabel lokal dengan nama yang tidak disebutkan dalam daftar. Daftar tangkap dapat berisi ini. Nama yang terdaftar tidak dapat didahului oleh &. Variabel yang disebutkan dalam   daftar penangkapan ditangkap oleh nilai.

[=, daftar tangkap]: secara implisit menangkap berdasarkan nilai semua variabel lokal dengan nama yang tidak disebutkan dalam daftar. Daftar tangkap tidak boleh berisi ini. Nama-nama yang tercantum harus didahului oleh &. Variabel yang disebutkan dalam daftar penangkapan ditangkap oleh referensi.

Perhatikan bahwa nama lokal yang didahului oleh & selalu ditangkap oleh   referensi dan nama lokal yang tidak diawasi oleh & selalu ditangkap oleh   nilai. Hanya menangkap dengan referensi memungkinkan modifikasi variabel dalam   lingkungan panggilan.

Additional

Lambda expression format

enter image description here

Referensi tambahan:


6
2017-11-09 11:02



Satu masalah yang diselesaikannya: Kode lebih sederhana dari lambda untuk panggilan di konstruktor yang menggunakan fungsi parameter output untuk menginisialisasi anggota const

Anda dapat menginisialisasi anggota const kelas Anda, dengan panggilan ke fungsi yang menetapkan nilainya dengan memberikan kembali outputnya sebagai parameter output.


1
2018-06-27 00:38