Mengapa lambda C ++ 11 memerlukan kata kunci "bisa berubah" untuk menangkap-oleh-nilai, secara default?

256

Contoh singkat:

#include <iostream>

int main()
{
    int n;
    [&](){n = 10;}();             // OK
    [=]() mutable {n = 20;}();    // OK
    // [=](){n = 10;}();          // Error: a by-value capture cannot be modified in a non-mutable lambda
    std::cout << n << "\n";       // "10"
}

Pertanyaannya: Mengapa kita perlu mutablekata kunci? Ini sangat berbeda dari melewati parameter tradisional ke fungsi bernama. Apa alasan di baliknya?

Saya mendapat kesan bahwa seluruh poin dari capture-by-value adalah untuk memungkinkan pengguna untuk mengubah sementara - jika tidak, saya hampir selalu lebih baik menggunakan capture-by-reference, bukan?

Adakah pencerahan?

(Omong-omong, saya menggunakan MSVC2010. AFAIK ini seharusnya standar)

kizzx2
sumber
101
Pertanyaan bagus; meskipun saya senang ada sesuatu yang akhirnya constsecara default!
xtofl
3
Bukan jawaban, tapi saya pikir ini adalah hal yang masuk akal: jika Anda mengambil sesuatu berdasarkan nilai, Anda tidak boleh mengubahnya hanya untuk menghemat 1 salinan ke variabel lokal. Setidaknya Anda tidak akan membuat kesalahan dengan mengubah n dengan mengganti = dengan &.
stefaanv
8
@xtofl: Tidak yakin itu baik, ketika segala sesuatu yang lain tidak constsecara default.
kizzx2
8
@ Tamás Szelei: Bukan untuk memulai argumen, tetapi IMHO konsep "mudah dipelajari" tidak memiliki tempat dalam bahasa C ++, terutama di zaman modern. Pokoknya: P
kizzx2
3
"seluruh titik tangkapan-oleh-nilai adalah untuk memungkinkan pengguna untuk mengubah sementara" - Tidak, intinya adalah bahwa lambda dapat tetap valid melampaui masa pakai dari setiap variabel yang ditangkap. Jika C ++ lambdas hanya memiliki capture-by-ref, mereka tidak akan dapat digunakan dalam banyak skenario.
Sebastian Redl

Jawaban:

230

Ini membutuhkan mutablekarena secara default, objek fungsi harus menghasilkan hasil yang sama setiap kali dipanggil. Ini adalah perbedaan antara fungsi berorientasi objek dan fungsi menggunakan variabel global, secara efektif.

Anak anjing
sumber
7
Ini poin yang bagus. Saya sangat setuju. Dalam C ++ 0x, saya tidak begitu mengerti bagaimana default membantu menegakkan di atas. Anggaplah saya berada di ujung penerima lambda, misalnya saya void f(const std::function<int(int)> g). Bagaimana saya dijamin bahwa gsebenarnya transparan referensial ? gPemasok mungkin sudah digunakan mutable. Jadi saya tidak akan tahu. Di sisi lain, jika default adalah non const, dan orang-orang harus menambahkan constbukannya mutableke objek fungsi, kompilator dapat benar-benar menegakkan const std::function<int(int)>bagian dan sekarang fdapat berasumsi bahwa gadalah const, tidak ada?
kizzx2
8
@ kizzx2: Di C ++, tidak ada yang ditegakkan , hanya disarankan. Seperti biasa, jika Anda melakukan sesuatu yang bodoh (persyaratan terdokumentasi untuk transparansi referensial dan kemudian melewati fungsi non-referensien-transparan), Anda mendapatkan apa pun yang datang kepada Anda.
Anak anjing
6
Jawaban ini membuka mata saya. Sebelumnya, saya berpikir bahwa dalam hal ini lambda hanya mengubah salinan untuk "jalankan" saat ini.
Zsolt Szatmari
4
@ZsoltSzatmari Komentar Anda membuka mata saya! : -DI tidak mendapatkan arti sebenarnya dari jawaban ini sampai saya membaca komentar Anda.
Jendas
5
Saya tidak setuju dengan premis dasar dari jawaban ini. C ++ tidak memiliki konsep "fungsi harus selalu mengembalikan nilai yang sama" di tempat lain dalam bahasa. Sebagai prinsip desain, saya setuju itu adalah cara yang baik untuk menulis fungsi, tapi saya tidak berpikir itu memegang air sebagai yang alasan untuk perilaku standar.
Ionoclast Brigham
103

Kode Anda hampir setara dengan ini:

#include <iostream>

class unnamed1
{
    int& n;
public:
    unnamed1(int& N) : n(N) {}

    /* OK. Your this is const but you don't modify the "n" reference,
    but the value pointed by it. You wouldn't be able to modify a reference
    anyway even if your operator() was mutable. When you assign a reference
    it will always point to the same var.
    */
    void operator()() const {n = 10;}
};

class unnamed2
{
    int n;
public:
    unnamed2(int N) : n(N) {}

    /* OK. Your this pointer is not const (since your operator() is "mutable" instead of const).
    So you can modify the "n" member. */
    void operator()() {n = 20;}
};

class unnamed3
{
    int n;
public:
    unnamed3(int N) : n(N) {}

    /* BAD. Your this is const so you can't modify the "n" member. */
    void operator()() const {n = 10;}
};

int main()
{
    int n;
    unnamed1 u1(n); u1();    // OK
    unnamed2 u2(n); u2();    // OK
    //unnamed3 u3(n); u3();  // Error
    std::cout << n << "\n";  // "10"
}

Jadi Anda bisa menganggap lambdas sebagai menghasilkan kelas dengan operator () yang default ke const kecuali Anda mengatakan bahwa itu bisa berubah.

Anda juga dapat menganggap semua variabel yang ditangkap di dalam [] (secara eksplisit atau implisit) sebagai anggota kelas itu: salinan objek untuk [=] atau referensi ke objek untuk [&]. Mereka diinisialisasi ketika Anda menyatakan lambda Anda seolah-olah ada konstruktor tersembunyi.

Daniel Munoz
sumber
5
Sementara penjelasan yang bagus tentang seperti apa a constatau mutablelambda jika diimplementasikan sebagai jenis yang didefinisikan pengguna yang setara, pertanyaannya adalah (seperti dalam judul dan diuraikan oleh OP dalam komentar) mengapa const default, jadi ini tidak menjawabnya.
underscore_d
36

Saya mendapat kesan bahwa seluruh poin dari capture-by-value adalah untuk memungkinkan pengguna untuk mengubah sementara - jika tidak, saya hampir selalu lebih baik menggunakan capture-by-reference, bukan?

Pertanyaannya adalah, apakah "hampir"? Kasus penggunaan yang sering muncul adalah untuk mengembalikan atau memberikan lambdas:

void registerCallback(std::function<void()> f) { /* ... */ }

void doSomething() {
  std::string name = receiveName();
  registerCallback([name]{ /* do something with name */ });
}

Saya pikir itu mutablebukan kasus "hampir". Saya menganggap "tangkapan-menurut-nilai" seperti "izinkan saya menggunakan nilainya setelah entitas yang ditangkap mati" daripada "izinkan saya mengubah salinannya". Tapi mungkin ini bisa diperdebatkan.

Johannes Schaub - litb
sumber
2
Contoh yang baik. Ini adalah use-case yang sangat kuat untuk penggunaan capture-by-value. Tapi mengapa itu default const? Apa tujuannya? mutabletampaknya keluar dari tempat di sini, ketika constadalah tidak default di "hampir" (: P) segala sesuatu yang lain dari bahasa.
kizzx2
8
@ kizzx2: Saya berharap constadalah default, setidaknya orang akan dipaksa untuk mempertimbangkan const-correctness: /
Matthieu M.
1
@ kizzx2 melihat ke dalam kertas lambda, menurut saya mereka membuatnya default constsehingga mereka bisa menyebutnya apakah objek lambda adalah const. Misalnya mereka bisa meneruskannya ke fungsi mengambil a std::function<void()> const&. Untuk memungkinkan lambda mengubah salinan yang diambil, dalam makalah awal, anggota data penutupan didefinisikan mutablesecara internal secara otomatis. Sekarang Anda harus secara manual memasukkan mutableekspresi lambda. Saya belum menemukan alasan terperinci.
Johannes Schaub - litb
2
Lihat open-std.org/JTC1/SC22/WG21/docs/papers/2008/n2651.pdf untuk beberapa detail.
Johannes Schaub - litb
5
Pada titik ini, bagi saya, jawaban / alasan "nyata" tampaknya adalah "mereka gagal mengerjakan detail implementasi": /
kizzx2
32

FWIW, Herb Sutter, seorang anggota terkenal dari komite standardisasi C ++, memberikan jawaban yang berbeda untuk pertanyaan itu dalam Masalah Lambda Correctness and Usability :

Pertimbangkan contoh pembuat jerami ini, di mana programmer menangkap variabel lokal berdasarkan nilai dan mencoba untuk memodifikasi nilai yang ditangkap (yang merupakan variabel anggota dari objek lambda):

int val = 0;
auto x = [=](item e)            // look ma, [=] means explicit copy
            { use(e,++val); };  // error: count is const, need ‘mutable’
auto y = [val](item e)          // darnit, I really can’t get more explicit
            { use(e,++val); };  // same error: count is const, need ‘mutable’

Fitur ini tampaknya telah ditambahkan karena kekhawatiran bahwa pengguna mungkin tidak menyadari bahwa ia mendapatkan salinan, dan khususnya bahwa karena lambda dapat disalin, ia mungkin mengubah salinan lambda yang berbeda.

Makalahnya adalah tentang mengapa ini harus diubah dalam C ++ 14. Ini singkat, ditulis dengan baik, layak dibaca jika Anda ingin tahu "apa yang ada di pikiran [anggota komite]" sehubungan dengan fitur khusus ini.

akim
sumber
16

Anda perlu memikirkan apa jenis penutupan fungsi Lambda Anda. Setiap kali Anda mendeklarasikan ekspresi Lambda, kompiler membuat tipe penutupan, yang tidak lain adalah deklarasi kelas tanpa nama dengan atribut ( lingkungan di mana ekspresi Lambda di mana dinyatakan) dan pemanggilan fungsi ::operator()diimplementasikan. Saat Anda menangkap variabel menggunakan nilai salin , kompiler akan membuat constatribut baru di tipe closure, jadi Anda tidak bisa mengubahnya di dalam ekspresi Lambda karena itu adalah atribut "read-only", itulah alasan mereka menyebutnya " penutupan ", karena dalam beberapa hal, Anda menutup ekspresi Lambda Anda dengan menyalin variabel dari cakupan atas ke dalam cakupan Lambda.mutable, entitas yang ditangkap akan menjadinon-constatribut tipe penutupan Anda. Inilah yang menyebabkan perubahan yang dilakukan dalam variabel yang dapat diubah yang ditangkap oleh nilai, untuk tidak disebarluaskan ke ruang lingkup atas, tetapi tetap di dalam stateful Lambda. Selalu mencoba membayangkan jenis penutupan yang dihasilkan dari ekspresi Lambda Anda, yang banyak membantu saya, dan saya harap itu dapat membantu Anda juga.

Tarantula
sumber
14

Lihat draf ini , di bawah 5.1.2 [expr.prim.lambda], subclause 5:

Tipe penutupan untuk ekspresi lambda memiliki operator fungsi panggilan inline publik (13.5.4) yang parameter dan tipe pengembaliannya dijelaskan oleh parameter-pernyataan-deklarasi-klausa dan masing-masing tipe trailingreturn. Operator pemanggilan fungsi ini dinyatakan const (9.3.1) jika dan hanya jika parameter-deklarasi-klausa lambdaexpression tidak diikuti oleh bisa berubah.

Sunting pada komentar litb: Mungkin mereka memikirkan capture-by-value sehingga perubahan luar pada variabel tidak tercermin di dalam lambda? Referensi bekerja dua arah, jadi itulah penjelasan saya. Tidak tahu apakah itu ada gunanya.

Sunting pada komentar kizzx2: Yang paling sering digunakan lambda adalah sebagai functor untuk algoritma. constNess default memungkinkannya digunakan dalam lingkungan yang konstan, sama seperti constfungsi normal-wajar dapat digunakan di sana, tetapi yang non- constwajar tidak bisa. Mungkin mereka hanya berpikir untuk membuatnya lebih intuitif untuk kasus-kasus itu, yang tahu apa yang terjadi dalam pikiran mereka. :)

Xeo
sumber
Itu standar, tetapi mengapa mereka menulis seperti ini?
kizzx2
@ kizzx2: Penjelasan saya langsung di bawah kutipan itu. :) Ini sedikit berhubungan dengan apa yang dikatakan litb tentang masa objek yang ditangkap, tetapi juga sedikit lebih jauh.
Xeo
@ Xeo: Oh ya, saya melewatkan itu: P Ini juga penjelasan yang bagus untuk penggunaan yang bagus dari nilai-nilai . Tetapi mengapa harus secara constdefault? Saya sudah mendapatkan salinan baru, sepertinya aneh untuk tidak membiarkan saya mengubahnya - terutama itu bukan sesuatu yang salah dengan itu - mereka hanya ingin saya tambahkan mutable.
kizzx2
Saya percaya ada upaya untuk membuat sintaks deklarasi fungsi genral baru, tampak seperti lambda. Itu juga seharusnya memperbaiki masalah lain dengan membuat semuanya const secara default. Tidak pernah selesai, tetapi ide-ide itu menepis definisi lambda.
Bo Persson
2
@ kizzx2 - Jika kita dapat memulai dari awal lagi, kita mungkin akan memiliki varkata kunci untuk memungkinkan perubahan dan konstan menjadi default untuk yang lainnya. Sekarang tidak, jadi kita harus hidup dengan itu. IMO, C ++ 2011 keluar dengan cukup baik, mengingat semuanya.
Bo Persson
11

Saya mendapat kesan bahwa seluruh poin dari capture-by-value adalah untuk memungkinkan pengguna untuk mengubah sementara - jika tidak, saya hampir selalu lebih baik menggunakan capture-by-reference, bukan?

nadalah tidak temporer. n adalah anggota objek fungsi lambda yang Anda buat dengan ekspresi lambda. Harapan standarnya adalah bahwa memanggil lambda Anda tidak mengubah keadaannya, oleh karena itu ia mencegah Anda memodifikasi secara tidak sengaja n.

Martin Ba
sumber
1
Seluruh objek lambda bersifat sementara, anggotanya juga memiliki masa hidup sementara.
Ben Voigt
2
@Ben: IIRC, saya merujuk pada masalah bahwa ketika seseorang mengatakan "sementara", saya memahaminya berarti objek sementara yang tidak disebutkan namanya , yang merupakan lambda itu sendiri, tetapi anggotanya tidak. Dan juga dari "dalam" lambda, tidak masalah apakah lambda itu sendiri bersifat sementara. Membaca ulang pertanyaannya meskipun tampaknya OP hanya bermaksud mengatakan "n di dalam lambda" ketika dia mengatakan "sementara".
Martin Ba
6

Anda harus memahami apa arti penangkapan! itu menangkap bukan lewat argumen! mari kita lihat beberapa contoh kode:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() {return x + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //output 10,20

}

Seperti yang Anda lihat meskipun xtelah diubah ke 20lambda masih kembali 10 ( xmasih 5di dalam lambda) Mengubah xdi dalam lambda berarti mengubah lambda itu sendiri pada setiap panggilan (lambda bermutasi pada setiap panggilan). Untuk menegakkan kebenaran, standar memperkenalkan mutablekata kunci. Dengan menentukan lambda sebagai bisa berubah, Anda mengatakan bahwa setiap panggilan ke lambda dapat menyebabkan perubahan dalam lambda itu sendiri. Coba lihat contoh lain:

int main()
{
    using namespace std;
    int x = 5;
    int y;
    auto lamb = [x]() mutable {return x++ + 5; };

    y= lamb();
    cout << y<<","<< x << endl; //outputs 10,5
    x = 20;
    y = lamb();
    cout << y << "," << x << endl; //outputs 11,20

}

Contoh di atas menunjukkan bahwa dengan membuat lambda bisa berubah, mengubah xdi dalam lambda "mengubah" lambda pada setiap panggilan dengan nilai baru xyang tidak ada hubungannya dengan nilai aktual xdalam fungsi utama

Mammar Soulimane
sumber
4

Sekarang ada proposal untuk mengurangi kebutuhan mutabledalam deklarasi lambda: n3424

usta
sumber
Adakah informasi tentang hal itu? Saya pribadi berpikir itu adalah ide yang buruk, karena "penangkapan ekspresi sewenang-wenang" yang baru menghaluskan sebagian besar poin rasa sakit.
Ben Voigt
1
@ BenVoigt Ya sepertinya perubahan demi perubahan.
Miles Rout
3
@ BenVoigt Meskipun untuk bersikap adil, saya berharap mungkin ada banyak pengembang C ++ yang tidak tahu bahwa mutableitu bahkan kata kunci dalam C ++.
Rute Mil
1

Untuk memperluas jawaban Puppy, fungsi lambda dimaksudkan sebagai fungsi murni . Itu berarti setiap panggilan yang diberikan set input unik selalu mengembalikan output yang sama. Mari kita tentukan input sebagai himpunan semua argumen plus semua variabel yang ditangkap saat lambda dipanggil.

Dalam fungsi murni, output semata-mata tergantung pada input dan bukan pada beberapa kondisi internal. Karena itu fungsi lambda, jika murni, tidak perlu mengubah kondisinya dan karenanya tidak dapat diubah.

Ketika lambda menangkap dengan referensi, menulis pada variabel yang ditangkap adalah tekanan pada konsep fungsi murni, karena semua fungsi murni harus dilakukan adalah mengembalikan output, meskipun lambda tidak pasti bermutasi karena penulisan terjadi pada variabel eksternal. Bahkan dalam kasus ini penggunaan yang benar menyiratkan bahwa jika lambda dipanggil dengan input yang sama lagi, output akan sama setiap kali, meskipun ada efek samping pada variabel by-ref. Efek samping semacam itu hanyalah cara untuk mengembalikan beberapa input tambahan (mis. Memperbarui penghitung) dan dapat diformulasi ulang menjadi fungsi murni, misalnya mengembalikan tupel alih-alih nilai tunggal.

Attersson
sumber