Mengapa f (i = -1, i = -1) perilaku tidak terdefinisi?


267

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, iadalah 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, iberakhir 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+1dan yang kedua i+2atau sebaliknya; di yang kedua, tidak jelas apakah iharus 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: iberakhir 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.


1
Objek skalar adalah objek tipe skalar. Lihat 3.9 / 9: "Tipe aritmatika (3.9.1), tipe enumerasi, tipe pointer, pointer ke tipe anggota (3.9.2) std::nullptr_t,, dan versi yang memenuhi syarat cv dari tipe-tipe ini (3.9.3) secara kolektif disebut tipe skalar . "
Rob Kennedy

1
Mungkin ada kesalahan pada halaman, dan itu sebenarnya berarti f(i-1, i = -1)atau sesuatu yang serupa.
Mr Lister


@RobKennedy Terima kasih. Apakah "tipe aritmatika" termasuk bool?
Nicu Stiurca

1
SchighSchagh pembaruan Anda harus di bagian jawaban.
Grijesh Chauhan

Jawaban:


343

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:

Jika A tidak diurutkan sebelum B dan B tidak diurutkan sebelum A, maka ada dua kemungkinan:

  • evaluasi A dan B tidak dilakukan: mereka dapat dilakukan dalam urutan apa pun dan mungkin tumpang tindih (dalam satu utas eksekusi, kompiler dapat menyisipkan instruksi CPU yang terdiri dari A dan B)

  • evaluasi A dan B secara berurutan ditentukan: mereka dapat dilakukan dalam urutan apa pun tetapi mungkin tidak tumpang tindih: baik A akan selesai sebelum B, atau B akan selesai sebelum A. Urutan mungkin berlawanan saat berikutnya ekspresi yang sama dievaluasi.

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:

f(i=-1, i=-1)

mungkin menjadi:

clear i
clear i
decr i
decr i

Sekarang saya adalah -2.

Ini mungkin contoh palsu, tetapi mungkin saja.


59
Contoh yang sangat bagus tentang bagaimana ekspresi sebenarnya bisa melakukan sesuatu yang tidak terduga sambil menyesuaikan dengan aturan pengurutan. Ya, sedikit dibuat-buat, tetapi begitu juga dengan kode snipped saya bertanya tentang di tempat pertama. :)
Nicu Stiurca

10
Dan bahkan jika penugasan dilakukan sebagai operasi atom, dimungkinkan untuk membayangkan arsitektur superscalar di mana kedua penugasan tersebut dilakukan secara bersamaan yang menyebabkan konflik akses memori yang mengakibatkan kegagalan. Bahasa ini dirancang sehingga penulis kompiler memiliki kebebasan sebanyak mungkin dalam menggunakan keunggulan mesin target.
ach

11
Saya sangat suka contoh Anda tentang bagaimana bahkan menetapkan nilai yang sama ke variabel yang sama di kedua parameter dapat menghasilkan hasil yang tidak terduga karena kedua penugasan tidak dilanjutkan
Martin J.

1
+1 e + 6 (ok, +1) untuk titik bahwa kode yang dikompilasi tidak selalu seperti yang Anda harapkan. Pengoptimal benar - benar pandai melempar kurva semacam ini kepada Anda ketika Anda tidak mengikuti aturan: P
Corey

3
Pada prosesor Arm, beban 32bit dapat memakan waktu hingga 4 instruksi: Itu bisa load 8bit immediate and shiftsampai 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).
ctrl-alt-delor

208

Pertama, "object skalar" berarti jenis seperti int, float, atau pointer (lihat Apa itu Obyek skalar di C ++? ).


Kedua, mungkin terlihat lebih jelas

f(++i, ++i);

akan memiliki perilaku yang tidak terdefinisi. Tapi

f(i = -1, i = -1);

kurang jelas.

Contoh yang sedikit berbeda:

int i;
f(i = 1, i = -1);
std::cout << i << "\n";

Tugas apa yang terjadi "terakhir" i = 1,, atau i = -1? Itu tidak didefinisikan dalam standar. Sungguh, itu ibisa berarti 5(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.

Jika efek samping pada objek skalar tidak diikuti relatif terhadap efek samping lain pada objek skalar yang sama, perilaku tidak terdefinisi.

Mereka bisa membuat pengecualian khusus untuk kasus khusus Anda, tetapi mereka tidak melakukannya. (Dan mengapa mereka? Penggunaan apa yang mungkin dimiliki?) Jadi, imasih bisa 5. 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.


9
@ DVD, ya, sebenarnya saya tahu bahwa jika Anda mengaktifkan -Wsequence-pointg ++, itu akan memperingatkan Anda.
Paul Draper

47
"Saya yakin g ++, Dentang, dan MSVC semua akan melakukan apa yang Anda harapkan" Saya tidak akan mempercayai kompiler modern. Mereka jahat. Misalnya mereka mungkin mengenali bahwa ini adalah perilaku yang tidak terdefinisi dan menganggap kode ini tidak dapat dijangkau. Jika mereka tidak melakukannya hari ini, mereka mungkin melakukannya besok. UB mana pun adalah bom waktu.
CodesInChaos

8
@ BlacklightShining "jawaban Anda buruk karena tidak baik" bukan umpan balik yang sangat berguna, bukan?
Vincent van der Weele

13
@BobJarvis Kompilasi sama sekali tidak memiliki kewajiban untuk menghasilkan kode yang benar bahkan jauh dalam menghadapi perilaku yang tidak terdefinisi. Bahkan dapat mengasumsikan bahwa kode ini bahkan tidak pernah dipanggil dan dengan demikian mengganti semuanya dengan nop (Perhatikan bahwa penyusun benar-benar membuat asumsi seperti itu di hadapan UB). Karenanya saya akan mengatakan bahwa reaksi yang benar terhadap laporan bug semacam itu hanya dapat "ditutup, berfungsi sebagaimana dimaksud"
Grizzly

7
@SchighSchagh Kadang-kadang pengulangan istilah (yang hanya di permukaan tampaknya menjadi jawaban tautologis) adalah apa yang orang butuhkan. Kebanyakan orang baru untuk spesifikasi teknis berpikir undefined behaviorberarti something random will happen, yang jauh dari kasus sebagian besar waktu.
Izkata

27

Alasan praktis untuk tidak membuat pengecualian dari aturan hanya karena kedua nilai tersebut sama:

// config.h
#define VALUEA  1

// defaults.h
#define VALUEB  1

// prog.cpp
f(i = VALUEA, i = VALUEB);

Pertimbangkan kasus ini diizinkan.

Sekarang, beberapa bulan kemudian, kebutuhan muncul untuk berubah

 #define VALUEB 2

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 Btidak diperbolehkan dalam beberapa bahasa, kapan B0, 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.


Tentu, tapi contoh tidak menggunakan bilangan bulat literal. Anda 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.
Wolf

3
@Wold Tetapi kompiler tidak melihat makro preprocessor. Dan bahkan jika tidak demikian, sulit untuk menemukan contoh dalam bahasa pemrograman mana pun, di mana kode sumber mengkompilasi hingga seseorang mengubah beberapa konstanta int dari 1 menjadi 2. Ini hanya tidak dapat diterima dan tidak dapat dijelaskan, sementara Anda melihat penjelasan yang sangat baik di sini mengapa kode itu rusak bahkan dengan nilai yang sama.
Ingo

Ya, kompilasi tidak melihat makro. Tapi, apakah ini pertanyaannya?
Wolf

1
Jawaban Anda tidak ada gunanya, bacalah jawaban yang berbahaya dan komentar OP tentang itu.
Wolf

1
Itu bisa dilakukan 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 menulis CONST C = X(2-A); FUNCTION X:INTEGER(CONST Y:INTEGER) = B/Y; ?? Atau apakah fungsi tidak diizinkan?
Ingo

12

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:

reg = 0xFF; // first instruction
reg |= 0xFF00; // second
reg |= 0xFF0000; // third
reg |= 0xFF000000; // fourth
i = reg; // last

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:

reg = 0xFF;
reg |= 0xFF00;
reg |= 0xFF0000;
reg = 0xFF;
reg |= 0xFF000000;
i = reg; // writes 0xFF0000FF == -16776961
reg |= 0xFF00;
reg |= 0xFF0000;
reg |= 0xFF000000;
i = reg; // writes 0xFFFFFFFF == -1

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.


Saya kira pada ARM kompiler hanya akan memuat konstanta dari sebuah tabel. Apa yang Anda gambarkan sepertinya lebih mirip MIPS.
ach

1
@AndreyChernyakhovskiy Yep, tetapi dalam kasus ketika itu tidak hanya -1(bahwa kompiler telah disimpan di suatu tempat), tetapi itu agak 3^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.
yo

@tohecz, ya, saya sudah memeriksanya. Memang, kompiler terlalu pintar untuk memuat setiap konstanta dari sebuah tabel. Bagaimanapun, itu tidak akan pernah menggunakan register yang sama untuk menghitung dua konstanta. Ini pasti akan 'mendefinisikan' perilaku yang didefinisikan.
ach

@AndreyChernyakhovskiy Tapi Anda mungkin tidak "setiap programmer compiler C ++ di dunia". Ingatlah bahwa ada mesin dengan 3 register pendek yang tersedia hanya untuk perhitungan.
yo

@tohecz, perhatikan contoh di f(i = A, j = B)mana idan jdua objek terpisah. Contoh ini tidak memiliki UB. Mesin yang memiliki 3 register pendek bukan alasan bagi kompiler untuk mencampurkan dua nilai Adan Bdalam register yang sama (seperti yang ditunjukkan dalam jawaban @ davidf), karena akan merusak semantik program.
ach

11

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:

uint8_t v;  // Global

void hey(uint8_t *p)
{
  moo(v=5, (*p)=6);
  zoo(v);
  zoo(v);
}

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.


9

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.


8

Sepertinya saya satu-satunya aturan yang berkaitan dengan urutan ekspresi argumen fungsi di sini:

3) Ketika memanggil suatu fungsi (apakah fungsinya inline atau tidak, dan apakah sintaks panggilan fungsi eksplisit digunakan atau tidak), setiap perhitungan nilai dan efek samping yang terkait dengan ekspresi argumen apa pun, atau dengan ekspresi postfix yang menunjuk fungsi yang dipanggil, adalah diurutkan sebelum dieksekusi setiap ekspresi atau pernyataan di tubuh fungsi yang disebut.

Ini tidak mendefinisikan urutan di antara ekspresi argumen, jadi kami berakhir dalam kasus ini:

1) Jika efek samping pada objek skalar tidak dipengaruhi relatif terhadap efek samping lain pada objek skalar yang sama, perilaku tidak terdefinisi.

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:

void f(int l, int r) {
    return l < -1;
}
auto b = f(i = -1, i = -2);
if (b) {
    formatDisk();
}

8

C ++ 17 mendefinisikan aturan evaluasi yang lebih ketat. Secara khusus, ia mengurutkan argumen fungsi (meskipun dalam urutan yang tidak ditentukan).

N5659 §4.6:15
Evaluasi A dan B secara tidak pasti diurutkan ketika salah satu A diurutkan sebelum B atau B diurutkan sebelum A , tetapi tidak ditentukan yang mana. [ Catatan : evaluasi yang tidak ditentukan secara berurutan tidak dapat tumpang tindih, tetapi keduanya dapat dieksekusi terlebih dahulu. - catatan akhir ]

N5659 § 8.2.2:5
Inisialisasi parameter, termasuk setiap perhitungan nilai yang terkait dan efek samping, secara tidak pasti diurutkan sehubungan dengan parameter lainnya.

Ini memungkinkan beberapa kasus yang akan menjadi UB sebelum:

f(i = -1, i = -1); // value of i is -1
f(i = -1, i = -2); // value of i is either -1 or -2, but not specified which one

2
Terima kasih telah menambahkan pembaruan ini untuk c ++ 17 , jadi saya tidak perlu melakukannya. ;)
Yakk - Adam Nevraumont

Luar biasa, terima kasih banyak atas jawaban ini. Sedikit tindak lanjut: jika ftanda tangan adalah f(int a, int b), apakah C ++ 17 menjamin itu a == -1dan b == -2jika disebut seperti dalam kasus kedua?
Nicu Stiurca

Iya. Jika kita memiliki parameter adan b, maka i-kemudian- adiinisialisasi ke -1, kemudian i-kemudian bdiinisialisasi ke -2, atau jalan di sekitar. Dalam kedua kasus, kita berakhir dengan a == -1dan b == -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 ".
AlexD

Saya pikir itu sudah sama di C sejak selamanya.
fuz

5

Operator penugasan dapat kelebihan beban, dalam hal ini pesanan dapat menjadi masalah:

struct A {
    bool first;
    A () : first (false) {
    }
    const A & operator = (int i) {
        first = !first;
        return * this;
    }
};

void f (A a1, A a2) {
    // ...
}


// ...
A i;
f (i = -1, i = -1);   // the argument evaluated first has ax.first == true

1
Cukup benar, tetapi pertanyaannya adalah tentang jenis skalar , yang telah ditunjukkan orang lain pada dasarnya berarti keluarga int, keluarga pelampung, dan pointer.
Nicu Stiurca

Masalah sebenarnya dalam kasus ini adalah bahwa operator penugasan adalah stateful, sehingga bahkan manipulasi variabel secara teratur rentan terhadap masalah seperti ini.
AJMansfield

2

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 .


Ini berbunyi seperti "jawaban tautan saja". Bisakah Anda menyalin bit yang relevan dari tautan itu ke jawaban ini (dalam blockquote)?
Cole Johnson

1
@ColeJohnson Ini bukan jawaban hanya tautan. Tautan ini hanya untuk penjelasan lebih lanjut. Jawaban saya adalah "pointer", "enum".
Peng Zhang

Saya tidak mengatakan jawaban Anda adalah link hanya menjawab. Saya mengatakannya "berbunyi seperti [satu]" . Saya sarankan Anda membaca mengapa kami tidak ingin hanya menautkan jawaban di bagian bantuan. Alasannya, jika Microsoft memperbarui URL mereka di situs mereka, tautan itu terputus.
Cole Johnson

1

Sebenarnya, ada alasan untuk tidak bergantung pada fakta bahwa kompiler akan memeriksa yang iditugaskan dengan nilai yang sama dua kali, sehingga dimungkinkan untuk menggantinya dengan penugasan tunggal. Bagaimana jika kita memiliki beberapa ekspresi?

void g(int a, int b, int c, int n) {
    int i;
    // hey, compiler has to prove Fermat's theorem now!
    f(i = 1, i = (ipow(a, n) + ipow(b, n) == ipow(c, n)));
}

1
Tidak perlu untuk membuktikan teorema Fermat: hanya menetapkan 1untuk i. Baik argumen menetapkan 1dan ini melakukan hal yang "benar", atau argumen memberikan nilai yang berbeda dan itu perilaku yang tidak terdefinisi sehingga pilihan kita masih diizinkan.
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.