Apakah ini perangkap C ++ 11 yang diketahui untuk loop?

89

Mari kita bayangkan kita memiliki sebuah struct untuk menampung 3 ganda dengan beberapa fungsi anggota:

struct Vector {
  double x, y, z;
  // ...
  Vector &negate() {
    x = -x; y = -y; z = -z;
    return *this;
  }
  Vector &normalize() {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  // ...
};

Ini sedikit dibuat-buat untuk kesederhanaan, tetapi saya yakin Anda setuju bahwa kode serupa ada di luar sana. Metode ini memungkinkan Anda untuk merangkai dengan mudah, misalnya:

Vector v = ...;
v.normalize().negate();

Atau bahkan:

Vector v = Vector{1., 2., 3.}.normalize().negate();

Sekarang jika kita menyediakan fungsi begin () dan end (), kita bisa menggunakan Vector kita dalam gaya baru for loop, katakanlah untuk melakukan loop pada 3 koordinat x, y, dan z (Anda pasti bisa membuat contoh yang lebih "berguna" dengan mengganti Vector dengan misalnya String):

Vector v = ...;
for (double x : v) { ... }

Kami bahkan dapat melakukan:

Vector v = ...;
for (double x : v.normalize().negate()) { ... }

dan juga:

for (double x : Vector{1., 2., 3.}) { ... }

Namun, berikut ini (menurut saya) rusak:

for (double x : Vector{1., 2., 3.}.normalize()) { ... }

Meskipun sepertinya kombinasi logis dari dua penggunaan sebelumnya, saya pikir penggunaan terakhir ini menciptakan referensi yang menggantung sementara dua penggunaan sebelumnya baik-baik saja.

  • Apakah ini benar dan dihargai secara luas?
  • Bagian mana di atas yang merupakan bagian "buruk", yang harus dihindari?
  • Akankah bahasa ditingkatkan dengan mengubah definisi loop berbasis rentang sedemikian rupa sehingga sementara yang dibangun dalam ekspresi-for ada selama durasi loop?
ndkrempel
sumber
Untuk beberapa alasan saya ingat pertanyaan yang sangat mirip ditanyakan sebelumnya, meskipun lupa apa namanya.
Pubby
Saya menganggap ini sebagai cacat bahasa. Masa pakai temporer tidak diperpanjang ke seluruh tubuh loop-for, tetapi hanya untuk pengaturan loop-for. Bukan hanya sintaks rentang yang menderita, sintaks klasik juga menderita. Menurut pendapat saya, masa pakai temporer dalam pernyataan init harus diperpanjang selama putaran penuh.
edA-qa mort-ora-y
1
@ edA-qamort-ora-y: Saya cenderung setuju bahwa ada sedikit cacat bahasa yang bersembunyi di sini, tapi menurut saya ini adalah fakta khusus bahwa perpanjangan seumur hidup terjadi secara implisit setiap kali Anda secara langsung mengikat sementara ke referensi, tetapi tidak dalam situasi lain - ini tampak seperti solusi setengah matang untuk masalah mendasar dari kehidupan sementara, meskipun itu tidak berarti sudah jelas solusi apa yang lebih baik. Mungkin sintaks 'perpanjangan seumur hidup' eksplisit saat membuat sementara, yang membuatnya bertahan hingga akhir blok saat ini - bagaimana menurut Anda?
ndkrempel
@ edA-qamort-ora-y: ... ini sama saja dengan mengikat sementara ke referensi, tetapi memiliki keuntungan karena lebih eksplisit bagi pembaca bahwa 'perpanjangan seumur hidup' terjadi, sebaris (dalam ekspresi , daripada membutuhkan deklarasi terpisah), dan tidak meminta Anda menyebutkan sementara.
ndkrempel
1
kemungkinan duplikat objek sementara dalam range-based untuk
ildjarn

Jawaban:

64

Apakah ini benar dan dihargai secara luas?

Ya, pemahaman Anda tentang berbagai hal sudah benar.

Bagian mana di atas yang merupakan bagian "buruk", yang harus dihindari?

Bagian yang buruk adalah mengambil referensi nilai-l ke sementara yang dikembalikan dari suatu fungsi, dan mengikatnya ke referensi nilai-r. Ini sama buruknya dengan ini:

auto &&t = Vector{1., 2., 3.}.normalize();

Masa Vector{1., 2., 3.}pakai sementara tidak dapat diperpanjang karena kompilator tidak tahu bahwa nilai yang dikembalikan dari normalizereferensi itu.

Akankah bahasa ditingkatkan dengan mengubah definisi loop berbasis rentang sedemikian rupa sehingga sementara yang dibangun dalam ekspresi-for ada selama durasi loop?

Itu akan sangat tidak konsisten dengan cara kerja C ++.

Akankah itu mencegah gotcha tertentu yang dibuat oleh orang-orang yang menggunakan ekspresi dirantai pada sementara atau berbagai metode evaluasi malas untuk ekspresi? Iya. Tetapi itu juga akan membutuhkan kode kompilator kasus khusus, serta membingungkan mengapa itu tidak bekerja dengan yang lain konstruksi ekspresi .

Solusi yang jauh lebih masuk akal adalah dengan memberi tahu kompiler bahwa nilai kembalian dari suatu fungsi selalu menjadi rujukan this , dan oleh karena itu jika nilai kembalian terikat ke konstruksi perluasan sementara, maka itu akan memperpanjang nilai sementara yang benar. Itu adalah solusi tingkat bahasa.

Saat ini (jika kompilator mendukungnya), Anda bisa membuatnya sehingga normalize tidak bisa dipanggil sementara:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector &normalize() && = delete;
};

Ini akan menyebabkan Vector{1., 2., 3.}.normalize()memberikan kesalahan kompilasi, sementara v.normalize()akan berfungsi dengan baik. Jelas Anda tidak akan bisa melakukan hal-hal yang benar seperti ini:

Vector t = Vector{1., 2., 3.}.normalize();

Tapi Anda juga tidak akan bisa melakukan hal yang salah.

Alternatifnya, seperti yang disarankan dalam komentar, Anda dapat membuat versi referensi rvalue mengembalikan nilai daripada referensi:

struct Vector {
  double x, y, z;
  // ...
  Vector &normalize() & {
     double s = 1./sqrt(x*x+y*y+z*z);
     x *= s; y *= s; z *= s;
     return *this;
  }
  Vector normalize() && {
     Vector ret = *this;
     ret.normalize();
     return ret;
  }
};

Jika Vectoradalah tipe dengan sumber daya yang sebenarnya untuk dipindahkan, Anda dapat menggunakan Vector ret = std::move(*this);sebagai gantinya. Optimalisasi nilai pengembalian bernama membuat ini cukup optimal dalam hal kinerja.

Nicol Bolas
sumber
1
Hal yang mungkin membuat ini lebih dari sebuah "gotcha" adalah bahwa perulangan for yang baru secara sintaksis menyembunyikan fakta bahwa pengikatan referensi sedang berlangsung di bawah sampul - yaitu jauh lebih tidak mencolok daripada contoh "sama buruknya" di atas. Itulah mengapa tampaknya masuk akal untuk menyarankan aturan perpanjangan seumur hidup tambahan, hanya untuk perulangan for yang baru.
ndkrempel
1
@ndkrempel: Ya, tetapi jika Anda akan mengusulkan fitur bahasa untuk memperbaikinya (dan karena itu harus menunggu setidaknya hingga 2017), saya lebih suka jika itu lebih komprehensif, sesuatu yang dapat menyelesaikan masalah ekstensi sementara di mana-mana .
Nicol Bolas
3
+1. Pada pendekatan terakhir, daripada deleteAnda dapat memberikan operasi alternatif yang mengembalikan nilai r: Vector normalize() && { normalize(); return std::move(*this); }(Saya percaya bahwa panggilan ke normalizedalam fungsi akan dikirim ke lvalue overload, tetapi seseorang harus memeriksanya :)
David Rodríguez - dribeas
3
Saya belum pernah melihat metode &/ &&kualifikasi ini. Apakah ini dari C ++ 11 atau ini beberapa ekstensi kompiler berpemilik (mungkin tersebar luas). Memberikan kemungkinan yang menarik.
Christian Rau
1
@ChristianRau: Ini baru untuk C ++ 11, dan analog dengan kualifikasi C ++ 03 "const" dan "volatile" dari fungsi anggota non-statis, yang dalam arti tertentu memenuhi kualifikasi "ini". Namun g ++ 4.7.0 tidak mendukungnya.
ndkrempel
25

untuk (double x: Vector {1., 2., 3.}. normalize ()) {...}

Itu bukan batasan bahasa, tapi masalah dengan kode Anda. Ekspresi Vector{1., 2., 3.}membuat sementara, tetapi normalizefungsi mengembalikan referensi lvalue . Karena ekspresinya adalah nilai l , kompilator mengasumsikan bahwa objek akan hidup, tetapi karena ini adalah referensi untuk sementara, objek mati setelah ekspresi penuh dievaluasi, jadi Anda akan mendapatkan referensi yang menggantung.

Sekarang, jika Anda mengubah desain Anda untuk mengembalikan objek baru berdasarkan nilai daripada referensi ke objek saat ini, maka tidak akan ada masalah dan kode akan bekerja seperti yang diharapkan.

David Rodríguez - dribeas
sumber
1
Apakah constreferensi akan memperpanjang umur objek dalam kasus ini?
David Stone
5
Yang akan mematahkan semantik yang diinginkan dengan jelas normalize()sebagai fungsi mutasi pada objek yang ada. Jadi pertanyaannya. Bahwa sementara memiliki "masa hidup yang diperpanjang" ketika digunakan untuk tujuan tertentu dari sebuah iterasi, dan bukan sebaliknya, menurut saya adalah kesalahan fitur yang membingungkan.
Andy Ross
2
@AndyRoss: Mengapa? Setiap ikatan sementara ke referensi nilai r (atau const&) diperpanjang masa pakainya.
Nicol Bolas
2
@ndkrempel: Masih, tidak pembatasan rentang berbasis untuk loop, masalah yang sama akan datang jika Anda mengikat referensi: Vector & r = Vector{1.,2.,3.}.normalize();. Desain Anda memiliki batasan itu, dan itu berarti Anda bersedia mengembalikan dengan nilai (yang mungkin masuk akal dalam banyak keadaan, dan terlebih lagi dengan rvalue-referensi dan pemindahan ), atau Anda perlu menangani masalah di tempat call: buat variabel yang tepat, lalu gunakan di for loop. Perhatikan juga bahwa ekspresi Vector v = Vector{1., 2., 3.}.normalize().negate();membuat dua objek ...
David Rodríguez - dribeas
1
@ DavidRodríguez-dribeas: masalah dengan pengikatan ke referensi-const adalah ini: T const& f(T const&);sepenuhnya baik-baik saja. T const& t = f(T());baik-baik saja. Dan kemudian, di TU lain Anda menemukan itu T const& f(T const& t) { return t; }dan Anda menangis ... Jika operator+beroperasi pada nilai-nilai, itu lebih aman ; maka kompilator dapat mengoptimalkan salinan (Want Speed? Pass by Values), tapi itu bonus. Satu-satunya pengikatan sementara yang saya izinkan adalah pengikatan ke referensi nilai-r, tetapi fungsi kemudian harus mengembalikan nilai untuk keamanan dan mengandalkan Salin Elisi / Semantik Pindah.
Matthieu M.
4

IMHO, contoh kedua sudah cacat. Operator pengubah mengembalikan *thisnyaman dengan cara yang Anda sebutkan: memungkinkan rantai pengubah. Ini dapat digunakan hanya dengan menyerahkan hasil modifikasi, tetapi melakukan ini rawan kesalahan karena dapat dengan mudah diabaikan. Jika saya melihat sesuatu seperti

Vector v{1., 2., 3.};
auto foo = somefunction1(v, 17);
auto bar = somefunction2(true, v, 2, foo);
auto baz = somefunction3(bar.quun(v), 93.2, v.qwarv(foo));

Saya tidak secara otomatis mencurigai bahwa fungsi berubah vsebagai efek samping. Tentu saja bisa , tapi akan membingungkan. Jadi jika saya menulis sesuatu seperti ini, saya akan memastikan itu vtetap konstan. Sebagai contoh Anda, saya akan menambahkan fungsi gratis

auto normalized(Vector v) -> Vector {return v.normalize();}
auto negated(Vector v) -> Vector {return v.negate();}

dan kemudian tulis loop

for( double x : negated(normalized(v)) ) { ... }

dan

for( double x : normalized(Vector{1., 2., 3}) ) { ... }

Itu IMO lebih mudah dibaca, dan lebih aman. Tentu saja, ini memerlukan salinan tambahan, namun untuk data yang dialokasikan dengan heap, hal ini mungkin dapat dilakukan dalam operasi pemindahan C ++ 11 yang murah.

kiri sekitar
sumber
Terima kasih. Seperti biasa, ada banyak pilihan. Satu situasi di mana saran Anda mungkin tidak dapat dijalankan adalah jika Vector adalah sebuah array (bukan heap yang dialokasikan) dari 1000 ganda, misalnya. Pertukaran efisiensi, kemudahan penggunaan, dan keamanan penggunaan.
ndkrempel
2
Ya, tetapi jarang berguna untuk memiliki struktur dengan ukuran> ≈100 pada stack.
kiri sekitar sekitar