Saya membaca tentang urutan pelanggaran evaluasi , dan mereka memberikan contoh yang membingungkan saya.
1) Jika efek samping pada objek skalar tidak berurutan relatif terhadap efek samping lain pada objek skalar yang sama, perilaku tidak terdefinisi.
// snip f(i = -1, i = -1); // undefined behavior
Dalam konteks ini, i
adalah objek skalar , yang tampaknya berarti
Tipe aritmatika (3.9.1), tipe enumerasi, tipe pointer, pointer ke tipe anggota (3.9.2), std :: nullptr_t, dan versi cv-kualifikasi dari tipe-tipe ini (3.9.3) secara kolektif disebut tipe skalar.
Saya tidak melihat bagaimana pernyataan itu ambigu dalam kasus itu. Tampak bagi saya bahwa terlepas dari apakah argumen pertama atau kedua dievaluasi terlebih dahulu, i
berakhir sebagai -1
, dan kedua argumen juga -1
.
Bisakah seseorang menjelaskan?
MEMPERBARUI
Saya sangat menghargai semua diskusi. Sejauh ini, saya sangat suka jawaban @ harmic karena ini mengungkap perangkap dan seluk-beluk mendefinisikan pernyataan ini terlepas dari bagaimana kelihatannya lurus ke depan pada pandangan pertama. @ acheong87 menunjukkan beberapa masalah yang muncul ketika menggunakan referensi, tapi saya pikir itu ortogonal dengan aspek efek samping yang tidak diketahui dari pertanyaan ini.
RINGKASAN
Karena pertanyaan ini mendapat banyak perhatian, saya akan merangkum poin / jawaban utama. Pertama, izinkan saya penyimpangan kecil untuk menunjukkan bahwa "mengapa" dapat telah terkait erat namun halus arti yang berbeda, yaitu "untuk apa penyebabnya ", "untuk apa alasan ", dan "untuk apa tujuan ". Saya akan mengelompokkan jawaban dengan mana dari makna "mengapa" yang mereka bahas.
untuk alasan apa
Jawaban utama di sini berasal dari Paul Draper , dengan Martin J menyumbangkan jawaban yang sama tetapi tidak luas. Jawaban Paul Draper sampai pada
Itu adalah perilaku yang tidak terdefinisi karena tidak didefinisikan apa perilaku itu.
Jawabannya secara keseluruhan sangat baik dalam menjelaskan apa yang dikatakan standar C ++. Ini juga membahas beberapa kasus terkait UB seperti f(++i, ++i);
dan f(i=1, i=-1);
. Dalam kasus pertama terkait, tidak jelas apakah argumen pertama harus i+1
dan yang kedua i+2
atau sebaliknya; di yang kedua, tidak jelas apakah i
harus 1 atau -1 setelah pemanggilan fungsi. Kedua kasus ini adalah UB karena berada di bawah aturan berikut:
Jika efek samping pada objek skalar tidak diikuti relatif terhadap efek samping lain pada objek skalar yang sama, perilaku tidak terdefinisi.
Oleh karena itu, f(i=-1, i=-1)
juga UB karena berada di bawah aturan yang sama, meskipun niat programmer (IMHO) jelas dan tidak ambigu.
Paul Draper juga membuatnya secara eksplisit dalam kesimpulannya itu
Mungkinkah perilaku itu didefinisikan? Iya. Apakah itu didefinisikan? Tidak.
yang membawa kita pada pertanyaan "untuk alasan / tujuan apa yang f(i=-1, i=-1)
tersisa sebagai perilaku yang tidak terdefinisi?"
untuk alasan / tujuan apa
Meskipun ada beberapa kekeliruan (mungkin ceroboh) dalam standar C ++, banyak kelalaian yang beralasan dan melayani tujuan tertentu. Walaupun saya sadar bahwa tujuannya sering kali adalah "membuat pekerjaan kompiler-penulis lebih mudah", atau "kode lebih cepat", saya terutama tertarik untuk mengetahui apakah ada alasan yang baik untuk meninggalkan f(i=-1, i=-1)
UB.
harmic dan supercat memberikan jawaban utama yang memberikan alasan bagi UB. Harmic menunjukkan bahwa kompiler pengoptimal yang mungkin memecah operasi penugasan atom ke dalam beberapa instruksi mesin, dan bahwa itu mungkin lebih lanjut interleave instruksi tersebut untuk kecepatan optimal. Ini dapat menyebabkan beberapa hasil yang sangat mengejutkan: i
berakhir sebagai -2 dalam skenarionya! Dengan demikian, harmic menunjukkan bagaimana menetapkan nilai yang sama ke variabel lebih dari satu kali dapat memiliki efek buruk jika operasi tidak dilanjutkan.
supercat memberikan paparan terkait tentang perangkap mencoba f(i=-1, i=-1)
untuk melakukan apa yang seharusnya dilakukan. Dia menunjukkan bahwa pada beberapa arsitektur, ada batasan keras terhadap beberapa penulisan simultan ke alamat memori yang sama. Kompiler mungkin kesulitan menangkap ini jika kita berurusan dengan sesuatu yang kurang sepele daripada f(i=-1, i=-1)
.
davidf juga memberikan contoh instruksi interleaving yang sangat mirip dengan yang berbahaya.
Meskipun masing-masing contoh berbahaya, supercat dan davidf agak dibuat-buat, secara bersama-sama mereka masih berfungsi untuk memberikan alasan nyata mengapa f(i=-1, i=-1)
harus perilaku yang tidak terdefinisi.
Saya menerima jawaban yang merugikan karena itu melakukan pekerjaan terbaik untuk mengatasi semua makna mengapa, meskipun jawaban Paul Draper membahas bagian "untuk alasan apa" dengan lebih baik.
jawaban lain
JohnB menunjukkan bahwa jika kita mempertimbangkan operator penugasan yang kelebihan beban (bukan hanya skalar biasa), maka kita dapat mengalami masalah juga.
sumber
std::nullptr_t
,, dan versi yang memenuhi syarat cv dari tipe-tipe ini (3.9.3) secara kolektif disebut tipe skalar . "f(i-1, i = -1)
atau sesuatu yang serupa.Jawaban:
Karena operasi tidak dilakukan, tidak ada yang mengatakan bahwa instruksi yang melakukan tugas tidak dapat disisipkan. Mungkin optimal untuk melakukannya, tergantung pada arsitektur CPU. Halaman yang dirujuk menyatakan ini:
Itu dengan sendirinya tidak tampak seperti itu akan menyebabkan masalah - dengan asumsi bahwa operasi yang dilakukan adalah menyimpan nilai -1 ke lokasi memori. Tetapi tidak ada yang mengatakan bahwa kompiler tidak dapat mengoptimalkannya menjadi seperangkat instruksi yang memiliki efek yang sama, tetapi dapat gagal jika operasi disisipkan dengan operasi lain pada lokasi memori yang sama.
Misalnya, bayangkan bahwa lebih efisien untuk mem-nolkan memori, lalu menurunkannya, dibandingkan dengan memuat nilai -1 in. Maka ini:
mungkin menjadi:
Sekarang saya adalah -2.
Ini mungkin contoh palsu, tetapi mungkin saja.
sumber
load 8bit immediate and shift
sampai 4 kali. Biasanya kompiler akan melakukan pengalamatan tidak langsung untuk mengambil nomor dari sebuah tabel untuk menghindari hal ini. (-1 dapat dilakukan dalam 1 instruksi, tetapi contoh lain dapat dipilih).Pertama, "object skalar" berarti jenis seperti
int
,float
, atau pointer (lihat Apa itu Obyek skalar di C ++? ).Kedua, mungkin terlihat lebih jelas
akan memiliki perilaku yang tidak terdefinisi. Tapi
kurang jelas.
Contoh yang sedikit berbeda:
Tugas apa yang terjadi "terakhir"
i = 1
,, ataui = -1
? Itu tidak didefinisikan dalam standar. Sungguh, itui
bisa berarti5
(lihat jawaban Harmik untuk penjelasan yang sepenuhnya masuk akal tentang bagaimana ini bisa terjadi). Atau program Anda dapat melakukan segmentasi. Atau format ulang hard drive Anda.Tetapi sekarang Anda bertanya: "Bagaimana dengan contoh saya? Saya menggunakan nilai yang sama (
-1
) untuk kedua tugas. Apa yang mungkin tidak jelas tentang itu?"Anda benar ... kecuali dalam cara komite standar C ++ menggambarkan ini.
Mereka bisa membuat pengecualian khusus untuk kasus khusus Anda, tetapi mereka tidak melakukannya. (Dan mengapa mereka? Penggunaan apa yang mungkin dimiliki?) Jadi,
i
masih bisa5
. Atau hard drive Anda bisa kosong. Jadi jawaban untuk pertanyaan Anda adalah:Itu adalah perilaku yang tidak terdefinisi karena tidak didefinisikan apa perilaku itu.
(Ini pantas ditekankan karena banyak programmer berpikir "tidak terdefinisi" berarti "acak", atau "tidak dapat diprediksi". Itu tidak; itu berarti tidak didefinisikan oleh standar. Perilaku itu bisa 100% konsisten, dan masih tidak dapat ditentukan.)
Mungkinkah perilaku itu didefinisikan? Iya. Apakah itu didefinisikan? Tidak. Karena itu, "tidak terdefinisi".
Yang mengatakan, "tidak terdefinisi" tidak berarti bahwa kompiler akan memformat hard drive Anda ... itu berarti bahwa itu bisa dan itu masih akan menjadi kompiler yang memenuhi standar. Secara realistis, saya yakin g ++, Dentang, dan MSVC semua akan melakukan apa yang Anda harapkan. Mereka tidak "harus".
Pertanyaan yang berbeda mungkin. Mengapa komite standar C ++ memilih untuk membuat efek samping ini tidak diurus? . Jawaban itu akan melibatkan sejarah dan pendapat komite. Atau Apa gunanya memiliki efek samping yang tidak diikutkan dalam C ++? , yang memungkinkan adanya pembenaran, baik itu alasan sebenarnya dari komite standar. Anda dapat mengajukan pertanyaan itu di sini, atau di programmers.stackexchange.com.
sumber
-Wsequence-point
g ++, itu akan memperingatkan Anda.undefined behavior
berartisomething random will happen
, yang jauh dari kasus sebagian besar waktu.Alasan praktis untuk tidak membuat pengecualian dari aturan hanya karena kedua nilai tersebut sama:
Pertimbangkan kasus ini diizinkan.
Sekarang, beberapa bulan kemudian, kebutuhan muncul untuk berubah
Tampaknya tidak berbahaya, bukan? Namun tiba-tiba prog.cpp tidak dapat dikompilasi lagi. Namun, kami merasa bahwa kompilasi seharusnya tidak bergantung pada nilai literal.
Intinya: tidak ada pengecualian pada aturan karena itu akan membuat kompilasi yang sukses tergantung pada nilai (bukan tipe) dari sebuah konstanta.
EDIT
@HeartWare menunjukkan bahwa ekspresi konstan dari formulir
A DIV B
tidak diperbolehkan dalam beberapa bahasa, kapanB
0, dan menyebabkan kompilasi gagal. Oleh karena itu perubahan konstanta dapat menyebabkan kesalahan kompilasi di beberapa tempat lain. Yang, IMHO, disayangkan. Tetapi tentu baik untuk membatasi hal-hal seperti itu pada hal-hal yang tidak dapat dihindari.sumber
f(i = VALUEA, i = VALUEB);
pasti memiliki potensi untuk perilaku yang tidak terdefinisi. Saya harap Anda tidak benar-benar mengkodekan nilai-nilai di belakang pengidentifikasi.SomeProcedure(A, B, B DIV (2-A))
. Bagaimanapun, jika bahasa menyatakan bahwa CONST harus sepenuhnya dievaluasi pada waktu kompilasi, maka, tentu saja, klaim saya tidak berlaku untuk kasus itu. Karena entah bagaimana mengaburkan perbedaan compiletime dan runtime. Apakah juga memperhatikan jika kita menulisCONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y;
?? Atau apakah fungsi tidak diizinkan?Kebingungannya adalah bahwa menyimpan nilai konstan ke dalam variabel lokal bukan merupakan instruksi atom pada setiap arsitektur yang dirancang untuk dijalankan oleh C. Prosesor yang dijalankan oleh kode lebih penting daripada kompiler dalam hal ini. Sebagai contoh, pada ARM di mana setiap instruksi tidak dapat membawa konstanta 32 bit lengkap, menyimpan sebuah int dalam sebuah variabel membutuhkan lebih dari satu instruksi. Contoh dengan kode pseudo ini di mana Anda hanya dapat menyimpan 8 bit pada suatu waktu dan harus bekerja dalam register 32 bit, saya adalah int32:
Anda dapat membayangkan bahwa jika kompiler ingin mengoptimalkannya mungkin interleave urutan yang sama dua kali, dan Anda tidak tahu nilai apa yang akan dituliskan kepada saya; dan katakanlah dia tidak terlalu pintar:
Namun dalam pengujian saya, gcc cukup baik untuk mengenali bahwa nilai yang sama digunakan dua kali dan menghasilkan sekali dan tidak melakukan sesuatu yang aneh. Saya mendapatkan -1, -1 Tapi contoh saya masih valid karena penting untuk mempertimbangkan bahwa bahkan konstanta mungkin tidak sejelas kelihatannya.
sumber
-1
(bahwa kompiler telah disimpan di suatu tempat), tetapi itu agak3^81 mod 2^32
, tetapi konstan, maka kompiler mungkin melakukan apa yang dilakukan di sini, dan dalam beberapa tuas omtimisasi, interleave urutan panggilan untuk menghindari menunggu.f(i = A, j = B)
manai
danj
dua objek terpisah. Contoh ini tidak memiliki UB. Mesin yang memiliki 3 register pendek bukan alasan bagi kompiler untuk mencampurkan dua nilaiA
danB
dalam register yang sama (seperti yang ditunjukkan dalam jawaban @ davidf), karena akan merusak semantik program.Perilaku umumnya ditentukan sebagai tidak terdefinisi jika ada beberapa alasan yang memungkinkan mengapa kompiler yang berusaha menjadi "membantu" dapat melakukan sesuatu yang akan menyebabkan perilaku yang sama sekali tidak terduga.
Dalam kasus di mana variabel ditulis berulang kali tanpa apa pun untuk memastikan bahwa penulisan terjadi pada waktu yang berbeda, beberapa jenis perangkat keras mungkin memungkinkan beberapa operasi "penyimpanan" dilakukan secara bersamaan ke alamat yang berbeda menggunakan memori port ganda. Namun, beberapa memori dual-port secara tegas melarang skenario di mana dua toko menekan alamat yang sama secara bersamaan, terlepas dari apakah nilai-nilai yang tertulis cocok atau tidak.. Jika kompiler untuk mesin seperti itu memperhatikan dua upaya yang belum dilakukan untuk menulis variabel yang sama, ia mungkin menolak untuk mengkompilasi atau memastikan bahwa kedua penulisan tidak dapat dijadwalkan secara bersamaan. Tetapi jika salah satu atau kedua akses adalah melalui sebuah pointer atau referensi, kompiler mungkin tidak selalu dapat mengetahui apakah kedua penulisan mungkin mengenai lokasi penyimpanan yang sama. Dalam hal itu, mungkin menjadwalkan menulis secara bersamaan, menyebabkan jebakan perangkat keras pada upaya akses.
Tentu saja, fakta bahwa seseorang mungkin mengimplementasikan kompiler C pada platform seperti itu tidak menunjukkan bahwa perilaku seperti itu tidak boleh didefinisikan pada platform perangkat keras ketika menggunakan toko jenis yang cukup kecil untuk diproses secara atom. Mencoba untuk menyimpan dua nilai berbeda dalam mode yang tidak diikuti dapat menyebabkan keanehan jika seorang kompiler tidak menyadarinya; misalnya, diberikan:
jika kompiler in-line panggilan ke "moo" dan dapat mengatakan itu tidak mengubah "v", itu mungkin menyimpan 5 ke v, kemudian menyimpan 6 ke * p, kemudian meneruskan 5 ke "kebun binatang", dan kemudian meneruskan isi v ke "zoo". Jika "kebun binatang" tidak mengubah "v", seharusnya tidak ada cara kedua panggilan harus melewati nilai yang berbeda, tetapi itu bisa dengan mudah terjadi. Di sisi lain, dalam kasus di mana kedua toko akan menulis nilai yang sama, keanehan seperti itu tidak dapat terjadi dan pada kebanyakan platform tidak ada alasan yang masuk akal untuk implementasi untuk melakukan sesuatu yang aneh. Sayangnya, beberapa penulis kompiler tidak memerlukan alasan untuk perilaku konyol di luar "karena Standar memperbolehkannya", sehingga kasus-kasus itu pun tidak aman.
sumber
Fakta bahwa hasilnya akan sama di sebagian besar implementasi dalam kasus ini bersifat insidental; urutan evaluasi masih belum ditentukan. Pertimbangkan
f(i = -1, i = -2)
: di sini, ketertiban penting. Satu-satunya alasan yang tidak penting dalam contoh Anda adalah kecelakaan karena kedua nilai tersebut-1
.Mengingat bahwa ekspresi ditentukan sebagai perilaku yang tidak terdefinisi, kompiler yang jahat dan patuh mungkin menampilkan gambar yang tidak pantas ketika Anda mengevaluasi
f(i = -1, i = -1)
dan membatalkan eksekusi - dan masih dianggap sepenuhnya benar. Untungnya, tidak ada kompiler yang saya sadari melakukannya.sumber
Sepertinya saya satu-satunya aturan yang berkaitan dengan urutan ekspresi argumen fungsi di sini:
Ini tidak mendefinisikan urutan di antara ekspresi argumen, jadi kami berakhir dalam kasus ini:
Dalam prakteknya, pada kebanyakan kompiler, contoh yang Anda kutip akan berjalan dengan baik (sebagai lawan dari "menghapus hard disk Anda" dan konsekuensi perilaku tidak terdefinisi teoretis lainnya).
Namun, ini merupakan kewajiban, karena ini tergantung pada perilaku penyusun tertentu, bahkan jika dua nilai yang ditetapkan adalah sama. Juga, jelas, jika Anda mencoba untuk menetapkan nilai yang berbeda, hasilnya akan "benar-benar" tidak terdefinisi:
sumber
C ++ 17 mendefinisikan aturan evaluasi yang lebih ketat. Secara khusus, ia mengurutkan argumen fungsi (meskipun dalam urutan yang tidak ditentukan).
Ini memungkinkan beberapa kasus yang akan menjadi UB sebelum:
sumber
f
tanda tangan adalahf(int a, int b)
, apakah C ++ 17 menjamin itua == -1
danb == -2
jika disebut seperti dalam kasus kedua?a
danb
, makai
-kemudian-a
diinisialisasi ke -1, kemudiani
-kemudianb
diinisialisasi ke -2, atau jalan di sekitar. Dalam kedua kasus, kita berakhir dengana == -1
danb == -2
. Setidaknya begitulah cara saya membaca " Inisialisasi parameter, termasuk setiap perhitungan nilai yang terkait dan efek samping, secara tidak pasti diurutkan sehubungan dengan parameter lainnya ".Operator penugasan dapat kelebihan beban, dalam hal ini pesanan dapat menjadi masalah:
sumber
Ini hanya menjawab "Saya tidak yakin apa" objek skalar "dapat berarti selain sesuatu seperti int atau float".
Saya akan menafsirkan "objek skalar" sebagai singkatan dari "objek tipe skalar", atau hanya "variabel tipe skalar". Kemudian,
pointer
,enum
(konstan) adalah tipe skalar.Ini adalah artikel MSDN dari Jenis Skalar .
sumber
Sebenarnya, ada alasan untuk tidak bergantung pada fakta bahwa kompiler akan memeriksa yang
i
ditugaskan dengan nilai yang sama dua kali, sehingga dimungkinkan untuk menggantinya dengan penugasan tunggal. Bagaimana jika kita memiliki beberapa ekspresi?sumber
1
untuki
. Baik argumen menetapkan1
dan ini melakukan hal yang "benar", atau argumen memberikan nilai yang berbeda dan itu perilaku yang tidak terdefinisi sehingga pilihan kita masih diizinkan.