Itu tidak terdefinisi karena ia memodifikasi x
dua kali antara titik urutan. Standar mengatakan itu tidak terdefinisi, oleh karena itu tidak terdefinisi.
Setahu saya itu.
Tapi kenapa?
Pemahaman saya adalah bahwa melarang hal ini memungkinkan kompiler untuk mengoptimalkan dengan lebih baik. Ini bisa masuk akal ketika C ditemukan, tetapi sekarang sepertinya argumen yang lemah.
Jika kita menemukan kembali C hari ini, apakah kita akan melakukannya dengan cara ini, atau dapatkah itu dilakukan dengan lebih baik?
Atau mungkin ada masalah yang lebih dalam, yang membuatnya sulit untuk mendefinisikan aturan yang konsisten untuk ekspresi seperti itu, jadi yang terbaik adalah melarangnya?
Jadi misalkan kita menciptakan kembali C hari ini. Saya ingin menyarankan aturan sederhana untuk ekspresi seperti x=x++
, yang menurut saya bekerja lebih baik daripada aturan yang ada.
Saya ingin mendapatkan pendapat Anda tentang aturan yang disarankan dibandingkan dengan yang ada, atau saran lainnya.
Aturan yang Disarankan:
- Di antara titik-titik urutan, urutan evaluasi tidak ditentukan.
- Efek samping segera terjadi.
Tidak ada perilaku tidak jelas yang terlibat. Ekspresi mengevaluasi nilai ini atau itu, tetapi pasti tidak akan memformat hard disk Anda (anehnya, saya belum pernah melihat implementasi di mana x=x++
memformat hard disk).
Contoh Ekspresi
x=x++
- Didefinisikan dengan baik, tidak berubahx
.
Pertama,x
ditambahkan (segera ketikax++
dievaluasi), lalu nilai lamanya disimpanx
.x++ + ++x
- Peningkatanx
dua kali, dievaluasi menjadi2*x+2
.
Meskipun kedua sisi dapat dievaluasi terlebih dahulu, hasilnya adalahx + (x+2)
(sisi kiri terlebih dahulu) atau(x+1) + (x+1)
(sisi kanan terlebih dahulu).x = x + (x=3)
- Tidak ditentukan,x
diatur ke salah satux+3
atau6
.
Jika sisi kanan dievaluasi dulu, itux+3
. Mungkin jugax=3
dievaluasi dulu, jadi begitu3+3
. Dalam kedua kasus,x=3
penugasan terjadi segera ketikax=3
dievaluasi, sehingga nilai yang disimpan ditimpa oleh penugasan lain.x+=(x=3)
- Didefinisikan dengan baik, setx
ke 6.
Anda bisa berpendapat bahwa ini hanya singkatan untuk ungkapan di atas.
Tetapi saya akan mengatakan bahwa itu+=
harus dieksekusi setelahx=3
, dan tidak dalam dua bagian (bacax
, evaluasix=3
, tambahkan, dan simpan nilai baru).
Apa Keuntungannya?
Beberapa komentar mengangkat poin bagus ini.
Saya tentu tidak berpikir ekspresi seperti x=x++
harus digunakan dalam kode normal apa pun.
Sebenarnya, aku jauh lebih ketat dari itu - saya pikir penggunaan hanya baik untuk x++
sebagai x++;
saja.
Namun, saya pikir aturan bahasanya harus sesederhana mungkin. Kalau tidak, programmer tidak akan memahaminya. aturan yang melarang mengubah variabel dua kali antara titik urut tentu saja merupakan aturan yang kebanyakan programmer tidak mengerti.
Aturan yang sangat mendasar adalah ini:
Jika A valid, dan B valid, dan digabungkan dengan cara yang valid, hasilnya valid.
x
adalah nilai-L yang valid, x++
apakah ekspresi yang valid, dan =
apakah cara yang valid untuk menggabungkan nilai-L dan ekspresi, jadi mengapa x=x++
tidak sah?
Standar C membuat pengecualian di sini, dan pengecualian ini memperumit aturan. Anda dapat mencari stackoverflow.com dan melihat seberapa banyak pengecualian ini membingungkan orang.
Jadi saya katakan - singkirkan kebingungan ini.
=== Ringkasan Jawaban ===
Kenapa melakukan itu?
Saya mencoba menjelaskan pada bagian di atas - Saya ingin aturan C menjadi sederhana.Potensi untuk pengoptimalan:
Ini memang membutuhkan kebebasan dari kompiler, tetapi saya tidak melihat apa pun yang meyakinkan saya bahwa itu mungkin signifikan.
Sebagian besar optimasi masih bisa dilakukan. Misalnya,a=3;b=5;
dapat dipesan ulang, meskipun standar menentukan pesanan. Ekspresi sepertia=b[i++]
masih dapat dioptimalkan dengan cara yang sama.Anda tidak dapat mengubah standar yang ada.
Saya akui, saya tidak bisa. Saya tidak pernah berpikir saya benar-benar dapat melanjutkan dan mengubah standar dan kompiler. Saya hanya ingin berpikir jika segala sesuatunya dapat dilakukan secara berbeda.
sumber
x
untuk dirinya sendiri, dan jika Anda ingin menambahx
Anda bisa mengatakanx++;
- tidak perlu untuk tugas itu. Saya akan mengatakannya tidak boleh didefinisikan hanya karena akan sulit untuk mengingat apa yang seharusnya terjadi.Jawaban:
Mungkin Anda harus menjawab pertanyaan pertama mengapa harus didefinisikan? Apakah ada keuntungan dalam gaya pemrograman, keterbacaan, pemeliharaan atau kinerja dengan memungkinkan ekspresi seperti itu dengan efek samping tambahan? Adalah
lebih mudah dibaca daripada
Mengingat bahwa perubahan seperti itu sangat mendasar dan melanggar basis kode yang ada.
sumber
Argumen yang membuat perilaku tidak terdefinisi ini memungkinkan optimalisasi yang lebih baik adalah tidak lemah hari ini. Bahkan, hari ini jauh lebih kuat daripada saat C masih baru.
Ketika C masih baru, mesin yang bisa memanfaatkan ini untuk optimasi yang lebih baik sebagian besar model teoritis. Orang-orang telah berbicara tentang kemungkinan membangun CPU di mana kompiler akan menginstruksikan CPU tentang instruksi apa yang bisa / harus dieksekusi secara paralel dengan instruksi lainnya. Mereka menunjuk fakta bahwa membiarkan ini memiliki perilaku yang tidak terdefinisi berarti bahwa pada CPU seperti itu, jika memang benar-benar ada, Anda dapat menjadwalkan bagian "kenaikan" dari instruksi untuk dieksekusi secara paralel dengan sisa aliran instruksi. Sementara mereka benar tentang teorinya, pada saat itu ada sedikit perangkat keras yang benar-benar dapat mengambil keuntungan dari kemungkinan ini.
Itu bukan hanya teori lagi. Sekarang ada perangkat keras dalam produksi, dan digunakan secara luas, (misalnya, Itanium, VLIW DSP) yang benar - benar dapat mengambil keuntungan dari ini. Mereka benar - benar melakukannya memungkinkan kompiler untuk menghasilkan aliran instruksi yang menentukan bahwa instruksi X, Y dan Z semuanya dapat dieksekusi secara paralel. Ini bukan lagi model teoritis - ini adalah perangkat keras nyata yang digunakan secara nyata untuk melakukan pekerjaan nyata.
IMO, membuat perilaku yang didefinisikan ini dekat dengan "solusi" terburuk untuk masalah ini. Anda jelas tidak boleh menggunakan ekspresi seperti ini. Untuk sebagian besar kode, perilaku ideal adalah kompilator hanya menolak ekspresi seperti itu sepenuhnya. Pada saat itu, kompiler C tidak melakukan analisis aliran yang diperlukan untuk mendeteksi hal itu secara andal. Bahkan pada saat standar C asli, itu masih sama sekali tidak umum.
Saya tidak yakin itu akan dapat diterima oleh komunitas saat ini juga - sementara banyak kompiler dapat melakukan analisis aliran semacam itu, mereka biasanya hanya melakukannya ketika Anda meminta optimasi. Saya ragu sebagian besar pemrogram akan menyukai ide memperlambat "debug" build hanya demi dapat menolak kode yang mereka (waras) tidak akan pernah menulis di tempat pertama.
Apa yang telah dilakukan C adalah pilihan terbaik kedua yang semi-masuk akal: beri tahu orang-orang untuk tidak melakukan itu, mengizinkan (tetapi tidak mengharuskan) kompiler untuk menolak kode. Ini menghindari (masih lebih jauh) memperlambat kompilasi untuk orang-orang yang tidak pernah menggunakannya, tetapi masih memungkinkan seseorang untuk menulis kompiler yang akan menolak kode tersebut jika mereka ingin (dan / atau memiliki bendera yang akan menolaknya sehingga orang dapat memilih untuk menggunakan atau tidak sesuai keinginan mereka).
Setidaknya IMO, membuat perilaku yang didefinisikan ini akan (setidaknya mendekati) keputusan terburuk yang mungkin dibuat. Pada perangkat keras gaya VLIW, pilihan Anda adalah menghasilkan kode yang lebih lambat untuk penggunaan yang wajar dari operator increment, hanya demi kode jelek yang melecehkan mereka, atau selalu memerlukan analisis aliran yang luas untuk membuktikan bahwa Anda tidak berurusan dengan kode jelek, sehingga Anda dapat menghasilkan kode lambat (serial) hanya jika benar-benar diperlukan.
Intinya: jika Anda ingin menyembuhkan masalah ini, Anda harus berpikir sebaliknya. Alih-alih mendefinisikan apa yang dilakukan kode seperti itu, Anda harus mendefinisikan bahasa sehingga ungkapan seperti itu tidak diperbolehkan sama sekali (dan hidup dengan kenyataan bahwa sebagian besar programmer mungkin akan memilih kompilasi yang lebih cepat daripada menegakkan persyaratan itu).
sumber
a=b[i++];
(misalnya) baik-baik saja, dan mengoptimalkannya adalah hal yang baik. Saya tidak, bagaimanapun, melihat titik melukai kode yang masuk akal seperti itu sehingga sesuatu seperti++i++
memiliki makna yang jelas.++i++
tepatnya adalah bahwa pada umumnya sulit untuk membedakannya dari ekspresi yang valid dengan efek samping (sepertia=b[i++]
). Ini mungkin tampak cukup sederhana bagi kita, tetapi jika saya mengingat Buku Naga dengan benar maka itu sebenarnya merupakan masalah yang sulit. Karena itulah perilaku ini adalah UB, bukan dilarang.Eric Lippert, seorang desainer utama di tim C # compiler, memposting di blognya sebuah artikel tentang sejumlah pertimbangan yang masuk ke dalam memilih untuk membuat fitur yang tidak terdefinisi pada tingkat spesifikasi bahasa. Jelas C # adalah bahasa yang berbeda, dengan faktor yang berbeda masuk ke dalam desain bahasanya, tetapi poin yang ia buat tetap relevan.
Secara khusus, ia menunjukkan masalah memiliki kompiler yang ada untuk bahasa yang memiliki implementasi yang sudah ada dan juga memiliki perwakilan di komite. Saya tidak yakin apakah itu yang terjadi di sini, tetapi cenderung relevan dengan sebagian besar diskusi spesifikasi terkait C dan C ++.
Yang juga perlu diperhatikan adalah, seperti yang Anda katakan, potensi kinerja untuk optimisasi kompiler. Meskipun benar bahwa kinerja CPU hari ini banyak urutan besarnya lebih besar daripada ketika C masih muda, sejumlah besar pemrograman C dilakukan hari ini dilakukan secara spesifik karena potensi peningkatan kinerja, dan potensi untuk (masa depan hipotetis) ) Optimalisasi instruksi CPU dan optimisasi pemrosesan multicore akan konyol untuk dicegah karena serangkaian aturan yang terlalu ketat untuk menangani efek samping dan titik urutan.
sumber
Pertama, mari kita lihat definisi perilaku yang tidak terdefinisi:
Jadi dengan kata lain, "perilaku tidak terdefinisi" hanya berarti bahwa kompiler bebas untuk menangani situasi dengan cara apa pun yang diinginkannya, dan tindakan semacam itu dianggap "benar".
Akar masalah yang dibahas adalah klausa berikut:
Penekanan ditambahkan.
Diberi ekspresi seperti
yang subexpressions
a++
,--b
,c
, dan++d
dapat dievaluasi dalam urutan apapun . Selanjutnya, efek samping daria++
,,--b
dan++d
dapat diterapkan pada titik mana pun sebelum titik urutan berikutnya (TKI, bahkan jikaa++
dievaluasi sebelumnya--b
, itu tidak dijamin yanga
akan diperbarui sebelum--b
dievaluasi). Seperti yang dikatakan orang lain, alasan perilaku ini adalah untuk memberikan implementasi kebebasan untuk mengatur ulang operasi secara optimal.Karena itu, bagaimanapun, ekspresi suka
dll., akan menghasilkan hasil yang berbeda untuk implementasi yang berbeda (atau untuk implementasi yang sama dengan pengaturan optimasi yang berbeda, atau berdasarkan pada kode di sekitarnya, dll.).
Perilaku dibiarkan tidak terdefinisi sehingga kompiler tidak memiliki kewajiban untuk "melakukan hal yang benar", apa pun itu. Kasus-kasus di atas cukup mudah untuk ditangkap, tetapi ada sejumlah kasus non-sepele yang akan sulit untuk ditangkap pada waktu kompilasi.
Jelas, Anda dapat merancang bahasa sedemikian rupa sehingga urutan evaluasi dan urutan di mana efek samping diterapkan secara ketat, dan Java dan C # melakukannya, sebagian besar untuk menghindari masalah yang mengarah pada definisi C dan C ++.
Jadi, mengapa perubahan ini tidak dilakukan pada C setelah 3 revisi standar? Pertama-tama, ada kode C warisan selama 40 tahun di luar sana, dan tidak dijamin bahwa perubahan seperti itu tidak akan merusak kode itu. Ini menempatkan sedikit beban pada penulis kompiler, karena perubahan seperti itu akan segera membuat semua kompiler yang ada tidak sesuai; setiap orang harus membuat penulisan ulang yang signifikan. Dan bahkan pada CPU modern yang cepat, masih mungkin untuk mewujudkan keuntungan kinerja nyata dengan mengutak-atik urutan evaluasi.
sumber
Pertama, Anda harus memahami bahwa bukan hanya x = x ++ yang tidak ditentukan. Tidak ada yang peduli dengan x = x ++, karena apa pun yang Anda definisikan, tidak ada gunanya. Apa yang tidak terdefinisi lebih seperti "a = b ++ di mana a dan b kebetulan sama" - yaitu
Ada beberapa cara berbeda fungsi dapat diimplementasikan, tergantung pada apa yang paling efisien untuk arsitektur prosesor (dan untuk pernyataan di sekitarnya, dalam hal ini adalah fungsi yang lebih kompleks daripada contoh). Misalnya, dua yang jelas:
atau
Perhatikan bahwa yang pertama terdaftar di atas, yang menggunakan lebih banyak instruksi dan lebih banyak register, adalah yang Anda perlukan untuk digunakan dalam semua kasus di mana a dan b tidak dapat dibuktikan berbeda.
sumber
b
sebelumnyaa
.Warisan
Asumsi bahwa C dapat diciptakan kembali hari ini tidak dapat diterima. Ada begitu banyak baris kode C yang telah diproduksi dan digunakan setiap hari, sehingga mengubah aturan permainan di tengah permainan itu salah.
Tentu saja Anda dapat menciptakan bahasa baru, katakanlah C + = , dengan aturan Anda. Tapi itu tidak akan menjadi C.
sumber
Menyatakan bahwa sesuatu didefinisikan tidak akan mengubah kompiler yang ada untuk menghormati definisi Anda. Itu terutama benar dalam kasus asumsi yang mungkin telah diandalkan secara eksplisit atau implisit di banyak tempat.
Masalah utama untuk asumsi ini bukan dengan
x = x++;
(kompiler dapat dengan mudah memeriksanya dan harus memperingatkan), itu dengan*p1 = (*p2)++
dan setara (p1[i] = p2[j]++;
ketika p1 dan p2 adalah parameter untuk suatu fungsi) di mana kompiler tidak dapat mengetahui dengan mudah jikap1 == p2
(dalam C99restrict
telah ditambahkan untuk menyebarkan kemungkinan mengasumsikan p1! = p2 di antara titik-titik urutan, sehingga dianggap bahwa kemungkinan pengoptimalan itu penting).sumber
p1[i]=p2[j]++
. Jika kompilator dapat menganggap tidak ada alias, tidak ada masalah. Jika tidak bisa, itu harus pergi dengan buku - kenaikanp2[j]
dulu, simpanp1[i]
nanti. Kecuali untuk peluang optimasi yang hilang, yang sepertinya tidak signifikan, saya tidak melihat masalah.x = x++;
belum ditulis tetapit = x; x++; x = t;
ataux=x; x++;
atau apa pun yang Anda inginkan sebagai semantik (tetapi bagaimana dengan diagnostik?). Untuk bahasa baru, cukup hilangkan efek samping.x++
sebagai titik urutan, seolah-olah itu adalah panggilan fungsiinc_and_return_old(&x)
akan melakukan trik.Dalam beberapa kasus, kode semacam ini didefinisikan dalam Standar C ++ 11 yang baru.
sumber
x = ++x
sekarang didefinisikan dengan baik (tetapi tidakx = x++
)