Apa yang membuat saya = i ++ +1; legal di C ++ 17?

186

Sebelum Anda mulai berteriak perilaku tidak terdefinisi, ini secara eksplisit terdaftar di N4659 (C ++ 17)

  i = i++ + 1;        // the value of i is incremented

Namun dalam N3337 (C ++ 11)

  i = i++ + 1;        // the behavior is undefined

Apa yang berubah?

Dari apa yang bisa saya kumpulkan, dari [N4659 basic.exec]

Kecuali jika disebutkan, evaluasi operan dari operator individual dan subekspresi dari ekspresi individu tidak dilakukan. [...] Perhitungan nilai operan operator diurutkan sebelum perhitungan nilai hasil operator. Jika efek samping pada lokasi memori tidak dilakukan relatif terhadap efek samping lain pada lokasi memori yang sama atau perhitungan nilai menggunakan nilai objek apa pun di lokasi memori yang sama, dan mereka tidak berpotensi bersamaan, perilaku tersebut tidak terdefinisi.

Di mana nilai didefinisikan pada [N4659 basic.type]

Untuk tipe yang dapat disalin sepele, representasi nilai adalah sekumpulan bit dalam representasi objek yang menentukan nilai , yang merupakan salah satu elemen diskrit dari serangkaian nilai yang ditentukan implementasi

Dari [N3337 basic.exec]

Kecuali jika disebutkan, evaluasi operan dari operator individual dan subekspresi dari ekspresi individu tidak dilakukan. [...] Perhitungan nilai operan operator diurutkan sebelum perhitungan nilai hasil operator. Jika efek samping pada objek skalar tidak diikutsertakan relatif terhadap efek samping lain pada objek skalar yang sama atau perhitungan nilai menggunakan nilai objek skalar yang sama, perilaku tidak terdefinisi.

Demikian juga, nilai didefinisikan pada [N3337 basic.type]

Untuk tipe yang dapat disalin sepele, representasi nilai adalah sekumpulan bit dalam representasi objek yang menentukan nilai , yang merupakan salah satu elemen diskrit dari serangkaian nilai yang ditentukan implementasi.

Mereka identik kecuali menyebutkan konkurensi yang tidak masalah, dan dengan penggunaan lokasi memori bukannya objek skalar , di mana

Tipe aritmatika, tipe enumerasi, tipe pointer, pointer ke tipe anggota, std::nullptr_t ,, dan versi yang memenuhi syarat cv dari tipe-tipe ini secara kolektif disebut tipe skalar.

Yang tidak mempengaruhi contoh.

Dari [N4659 expr.ass]

Operator penugasan (=) dan operator penugasan majemuk semuanya grup kanan-ke-kiri. Semua membutuhkan nilai yang dapat dimodifikasi sebagai operan kiri mereka dan mengembalikan nilai yang mengacu pada operan kiri. Hasilnya dalam semua kasus adalah bidang-bit jika operan kiri adalah bidang-sedikit. Dalam semua kasus, penugasan diurutkan setelah perhitungan nilai operan kanan dan kiri, dan sebelum perhitungan nilai ekspresi penugasan. Operan kanan diurutkan sebelum operan kiri.

Dari [N3337 expr.ass]

Operator penugasan (=) dan operator penugasan majemuk semuanya grup kanan-ke-kiri. Semua membutuhkan nilai yang dapat dimodifikasi sebagai operan kiri mereka dan mengembalikan nilai yang mengacu pada operan kiri. Hasilnya dalam semua kasus adalah bidang-bit jika operan kiri adalah bidang-sedikit. Dalam semua kasus, penugasan diurutkan setelah perhitungan nilai operan kanan dan kiri, dan sebelum perhitungan nilai ekspresi penugasan.

Satu-satunya perbedaan adalah kalimat terakhir yang tidak ada di N3337.

Namun kalimat terakhir, seharusnya tidak memiliki kepentingan karena operan kiri ibukanlah "efek samping lain" atau "menggunakan nilai objek skalar yang sama" karena ekspresi id adalah lvalue.

Melewati
sumber
23
Anda telah mengidentifikasi alasan mengapa: Di C ++ 17, operan kanan diurutkan sebelum operan kiri. Di C ++ 11 tidak ada urutan seperti itu. Apa tepatnya pertanyaan Anda?
Robᵩ
4
@ Robᵩ Lihat kalimat terakhir.
Passer By
7
Adakah yang punya tautan ke motivasi untuk perubahan ini? Saya ingin penganalisa statis untuk dapat mengatakan "Anda tidak ingin melakukan itu" ketika dihadapkan dengan kode seperti i = i++ + 1;.
7
@NeilButterworth, ini dari kertas p0145r3.pdf : "Menyempurnakan Pesanan Evaluasi Ekspresi untuk Idiomatic C ++".
xaizek
9
@NeilButterworth, bagian nomor 2 mengatakan bahwa ini adalah kontra intuitif dan bahkan para ahli gagal melakukan hal yang benar dalam semua kasus. Itu semua motivasi mereka.
xaizek

Jawaban:

144

Dalam C ++ 11 tindakan "penugasan", yaitu efek samping dari memodifikasi LHS, diurutkan setelah perhitungan nilai operan yang tepat. Perhatikan bahwa ini adalah jaminan yang relatif "lemah": hanya menghasilkan urutan dengan kaitannya dengan perhitungan nilai RHS. Ia tidak mengatakan apa-apa tentang efek samping yang mungkin ada dalam RHS, karena terjadinya efek samping bukan bagian dari perhitungan nilai . Persyaratan C ++ 11 tidak menetapkan urutan relatif antara tindakan penugasan dan efek samping RHS. Inilah yang menciptakan potensi bagi UB.

Satu-satunya harapan dalam hal ini adalah jaminan tambahan yang dibuat oleh operator tertentu yang digunakan dalam RHS. Jika RHS menggunakan awalan ++, mengurutkan properti khusus untuk bentuk awalan ++akan menyimpan hari dalam contoh ini. Tetapi postfix ++adalah cerita yang berbeda: postfix tidak memberikan jaminan seperti itu. Dalam C ++ 11 efek samping dari =dan postfix ++berakhir tanpa hubungan dengan satu sama lain dalam contoh ini. Dan itu adalah UB.

Dalam C ++ 17 kalimat tambahan ditambahkan ke spesifikasi operator penugasan:

Operan kanan diurutkan sebelum operan kiri.

Dikombinasikan dengan hal-hal di atas, ini memberikan jaminan yang sangat kuat. Ini mengurutkan semua yang terjadi di RHS (termasuk efek samping) sebelum semua yang terjadi di LHS. Karena penugasan aktual diurutkan setelah LHS (dan RHS), sekuensing ekstra sepenuhnya mengisolasi tindakan penugasan dari setiap efek samping yang ada dalam RHS. Urutan yang lebih kuat inilah yang menghilangkan UB di atas.

(Diperbarui untuk mempertimbangkan komentar @John Bollinger.)

Semut
sumber
3
Apakah benar memasukkan "tindakan penugasan yang sebenarnya" dalam efek yang dicakup oleh "operan tangan kiri" dalam kutipan itu? Standar memiliki bahasa terpisah tentang urutan tugas yang sebenarnya. Saya mengambil kutipan yang Anda sajikan terbatas dalam cakupan urutan sub-ekspresi kiri dan kanan, yang tampaknya tidak cukup, dalam kombinasi dengan sisa bagian itu, untuk mendukung dengan baik definisi pernyataan OP.
John Bollinger
11
Koreksi: penugasan aktual masih diurutkan setelah perhitungan nilai operan kiri, dan evaluasi operan kiri diurutkan setelah (selesai) evaluasi operan kanan, jadi ya, perubahan itu cukup untuk mendukung definisi OP yang jelas. bertanya mengenai. Saya hanya kebawelan rincian, maka, tetapi ini penting, karena mereka mungkin memiliki implikasi yang berbeda untuk kode yang berbeda.
John Bollinger
3
@JohnBollinger: Saya merasa penasaran bahwa penulis Standar akan membuat perubahan yang merusak efisiensi pembuatan kode langsung dan secara historis tidak diperlukan, namun menolak untuk mendefinisikan perilaku lain yang ketidakhadirannya adalah masalah yang jauh lebih besar, dan yang jarang menimbulkan hambatan berarti bagi efisiensi.
supercat
1
@ Ka: Untuk penugasan majemuk, melakukan evaluasi nilai sisi kiri setelah sisi kanan memungkinkan sesuatu seperti x -= y;untuk diproses mov eax,[y] / sub [x],eaxdaripada mov eax,[x] / neg eax / add eax,[y] / mov [x],eax. Saya tidak melihat sesuatu yang bodoh tentang hal itu. Jika seseorang harus menentukan urutan, urutan paling efisien mungkin akan melakukan semua perhitungan yang diperlukan untuk mengidentifikasi objek sisi kiri terlebih dahulu, kemudian mengevaluasi operan kanan, kemudian nilai objek kiri, tetapi itu akan memerlukan memiliki istilah untuk tindakan menyelesaikan id'ty objek kiri.
supercat
1
@ Ka: Jika xdan ydulu volatile, itu akan memiliki efek samping. Lebih lanjut, pertimbangan yang sama akan berlaku untuk x += f();, jika f()dimodifikasi x.
supercat
33

Anda mengidentifikasi kalimat baru

Operan kanan diurutkan sebelum operan kiri.

dan Anda mengidentifikasi dengan benar bahwa evaluasi operan kiri sebagai nilai tidak relevan. Namun, diurutkan sebelum ditetapkan menjadi hubungan transitif. Oleh karena itu operan lengkap yang lengkap (termasuk kenaikan-pos) juga diurutkan sebelum penugasan. Dalam C ++ 11, hanya perhitungan nilai operan kanan yang diurutkan sebelum penugasan.


sumber
7

Dalam standar C ++ yang lebih lama dan dalam C11, definisi teks operator penugasan berakhir dengan teks:

Evaluasi operan tidak dilakukan.

Berarti bahwa efek samping dalam operan tidak diikuti dan karenanya perilaku yang tidak terdefinisi jika mereka menggunakan variabel yang sama.

Teks ini hanya dihapus di C ++ 11, meninggalkannya agak ambigu. Apakah ini UB atau bukan? Ini telah diklarifikasi dalam C ++ 17 di mana mereka menambahkan:

Operan kanan diurutkan sebelum operan kiri.


Sebagai catatan tambahan, bahkan dalam standar yang lebih lama, ini semua dibuat sangat jelas, contoh dari C99:

Urutan evaluasi operan tidak ditentukan. Jika suatu upaya dilakukan untuk memodifikasi hasil dari operator penugasan atau untuk mengaksesnya setelah titik urutan berikutnya, perilaku tidak terdefinisi.

Pada dasarnya, di C11 / C ++ 11, mereka mengacaukan ketika mereka menghapus teks ini.

Lundin
sumber
1

Ini adalah informasi lebih lanjut untuk jawaban lain dan saya mempostingnya karena kode di bawah ini sering ditanyakan juga .

Penjelasan dalam jawaban lain benar dan juga berlaku untuk kode berikut yang sekarang didefinisikan dengan baik (dan tidak mengubah nilai yang disimpan i):

i = i++;

Ini + 1adalah herring merah dan tidak terlalu jelas mengapa Standar menggunakannya dalam contoh mereka, meskipun saya ingat orang berdebat di milis sebelum C ++ 11 yang mungkin + 1membuat perbedaan karena memaksa konversi nilai awal di kanan- sisi tangan. Tentu saja tidak ada yang berlaku di C ++ 17 (dan mungkin tidak pernah diterapkan dalam versi C ++).

MM
sumber