Perbedaan perilaku penangkapan fungsi lambda dari referensi ke variabel global

22

Saya menemukan hasilnya berbeda di kompiler jika saya menggunakan lambda untuk menangkap referensi ke variabel global dengan kata kunci yang bisa berubah dan kemudian memodifikasi nilai dalam fungsi lambda.

#include <stdio.h>
#include <functional>

int n = 100;

std::function<int()> f()
{
    int &m = n;
    return [m] () mutable -> int {
        m += 123;
        return m;
    };
}

int main()
{
    int x = n;
    int y = f()();
    int z = n;

    printf("%d %d %d\n", x, y, z);
    return 0;
}

Hasil dari VS 2015 dan GCC (g ++ (Ubuntu 5.4.0-6ubuntu1 ~ 16.04.12) 5.4.0 20160609):

100 223 100

Hasil dari dentang ++ (dentang versi 3.8.0-2ubuntu4 (tag / RELEASE_380 / final)):

100 223 223

Mengapa ini terjadi? Apakah ini diizinkan oleh Standar C ++?

Willy
sumber
Perilaku dentang masih ada di bagasi.
walnut
Ini semua adalah versi kompiler yang agak lama
MM
Itu masih menghadirkan Clang versi terbaru: godbolt.org/z/P9na9c
Willy
1
Jika Anda menghapus tangkapan sepenuhnya, maka GCC masih menerima kode ini dan melakukan apa yang dilakukan dentang. Itu petunjuk kuat bahwa ada bug GCC - penangkapan sederhana tidak seharusnya mengubah arti tubuh lambda.
TC

Jawaban:

16

Lambda tidak dapat menangkap referensi itu sendiri berdasarkan nilai (digunakan std::reference_wrapperuntuk tujuan itu).

Di lambda Anda, [m]menangkap mdengan nilai (karena tidak ada &dalam penangkapan), jadi m(menjadi referensi n) pertama kali direferensikan dan salinan dari hal yang direferensikan ( n) ditangkap. Ini tidak berbeda dengan melakukan ini:

int &m = n;
int x = m; // <-- copy made!

Lambda kemudian memodifikasi salinan itu, bukan yang asli. Itulah yang Anda lihat terjadi dalam output VS dan GCC, seperti yang diharapkan.

Output dentang salah, dan harus dilaporkan sebagai bug, jika belum.

Jika Anda ingin lambda Anda untuk memodifikasi n, capture mdengan referensi sebagai gantinya: [&m]. Ini tidak berbeda dengan menugaskan satu referensi ke referensi lain, misalnya:

int &m = n;
int &x = m; // <-- no copy made!

Atau, Anda hanya dapat menyingkirkan msama sekali dan menangkap ndengan referensi sebagai gantinya: [&n].

Meskipun, karena nberada dalam lingkup global, itu benar-benar tidak perlu ditangkap sama sekali, lambda dapat mengaksesnya secara global tanpa menangkapnya:

return [] () -> int {
    n += 123;
    return n;
};
Remy Lebeau
sumber
5

Saya pikir Dentang mungkin benar.

Menurut [lambda.capture] / 11 , ekspresi-id yang digunakan dalam lambda mengacu pada anggota yang diambil oleh-lambda hanya jika itu merupakan penggunaan odr . Jika tidak, maka mengacu pada entitas asli . Ini berlaku untuk semua versi C ++ sejak C ++ 11.

Menurut C ++ 17 [basic.dev.odr] / 3 variabel referensi tidak digunakan jika menggunakan konversi lvalue-ke-rvalue akan menghasilkan ekspresi yang konstan.

Namun dalam konsep C ++ 20 persyaratan untuk konversi nilai-ke-nilai turun dan bagian yang relevan diubah beberapa kali untuk menyertakan atau tidak menyertakan konversi. Lihat masalah CWG 1472 dan masalah CWG 1741 , serta masalah CWG terbuka 2083 .

Karena mdiinisialisasi dengan ekspresi konstan (merujuk ke objek durasi penyimpanan statis), menggunakannya menghasilkan ekspresi konstan per pengecualian dalam [expr.const] /2.11.1 .

Namun, ini bukan masalahnya jika konversi nilai-ke-nilai diterapkan, karena nilai ntidak dapat digunakan dalam ekspresi konstan.

Oleh karena itu, tergantung pada apakah konversi nilai-ke-nilai atau tidak seharusnya diterapkan dalam menentukan penggunaan odr, ketika Anda menggunakannya mdi lambda, itu mungkin merujuk pada anggota lambda atau tidak.

Jika konversi harus diterapkan, GCC dan MSVC sudah benar, jika tidak, Dentang adalah.

Anda dapat melihat bahwa Dentang mengubah perilaku itu jika Anda mengubah inisialisasi mmenjadi tidak ekspresi konstan lagi:

#include <stdio.h>
#include <functional>

int n = 100;

void g() {}

std::function<int()> f()
{
    int &m = (g(), n);
    return [m] () mutable -> int {
        m += 123;
        return m;
    };
}

int main()
{
    int x = n;
    int y = f()();
    int z = n;

    printf("%d %d %d\n", x, y, z);
    return 0;
}

Dalam hal ini semua kompiler setuju bahwa outputnya adalah

100 223 100

karena mdalam lambda akan merujuk ke anggota penutupan yang merupakan jenis intsalin diinisialisasi dari variabel referensi mdi f.

kenari
sumber
Apakah kedua hasil VS / GCC & Dentang benar? Atau hanya satu dari mereka?
Willy
[basic.dev.odr] / 3 mengatakan variabel mtersebut odr-digunakan oleh ekspresi penamaan kecuali jika menerapkan konversi lvalue-ke-rvalue akan menjadi ekspresi konstan. Dengan [expr.const] / (2.7), konversi itu tidak akan menjadi ekspresi konstanta inti.
aschepler
Jika hasil Clang benar, saya pikir itu entah bagaimana berlawanan dengan intuisi. Karena dari sudut pandang programmer, ia perlu memastikan variabel yang ia tulis dalam daftar tangkap benar-benar disalin untuk kasus yang bisa berubah, dan inisialisasi m mungkin akan diubah oleh programmer nanti untuk beberapa alasan.
Willy
1
m += 123;Di sini mdigunakan odr.
Oliv
1
Saya pikir Dentang benar dengan kata-kata saat ini, dan meskipun saya belum menggali ini, perubahan yang relevan di sini hampir pasti semua DR.
TC
4

Ini tidak diizinkan oleh Standar C ++ 17, tetapi oleh beberapa konsep Standar lain mungkin. Ini rumit, karena alasan yang tidak dijelaskan dalam jawaban ini.

[expr.prim.lambda.capture] / 10 :

Untuk setiap entitas yang ditangkap melalui salinan, anggota data non-statis yang tidak disebutkan namanya dinyatakan dalam tipe penutupan. Urutan deklarasi anggota ini tidak ditentukan. Jenis anggota data tersebut adalah tipe yang direferensikan jika entitas merupakan referensi ke objek, referensi nilai untuk tipe fungsi yang direferensikan jika entitas tersebut merupakan referensi ke suatu fungsi, atau jenis entitas yang ditangkap yang sesuai sebaliknya.

The [m]berarti bahwa variabel mdalam fditangkap oleh copy. Entitas madalah referensi ke objek, jadi tipe penutupan memiliki anggota yang tipenya adalah tipe yang direferensikan. Yaitu, tipe anggota adalah int, dan bukan int&.

Karena nama mdi dalam tubuh lambda menamai anggota objek penutupan dan bukan variabel di f(dan ini adalah bagian yang dipertanyakan), pernyataan m += 123;memodifikasi anggota itu, yang merupakan intobjek yang berbeda ::n.

aschepler
sumber