Filosofi di balik Perilaku Tidak Terdefinisi


59

Spesifikasi C \ C ++ membuat sejumlah besar perilaku terbuka bagi kompiler untuk diimplementasikan dengan cara mereka sendiri. Ada sejumlah pertanyaan yang selalu ditanyakan di sini tentang hal yang sama dan kami memiliki beberapa posting yang sangat baik tentang hal itu:

Pertanyaan saya bukan tentang apa perilaku tidak terdefinisi itu, atau apakah itu benar-benar buruk. Saya tahu bahaya dan sebagian besar kutipan perilaku tidak terdefinisi yang relevan dari standar, jadi tolong jangan posting jawaban tentang seberapa buruk itu. Pertanyaan ini adalah tentang filosofi di balik membiarkan begitu banyak perilaku terbuka untuk implementasi kompiler.

Saya membaca posting blog yang sangat bagus yang menyatakan bahwa kinerja adalah alasan utama. Saya bertanya-tanya apakah kinerja adalah satu-satunya kriteria untuk mengizinkannya, atau adakah faktor lain yang mempengaruhi keputusan untuk membiarkan hal-hal terbuka untuk implementasi kompiler?

Jika Anda memiliki contoh untuk dikutip tentang bagaimana perilaku tertentu yang tidak terdefinisi menyediakan ruang yang cukup untuk dioptimalkan oleh kompiler, harap daftarkan mereka. Jika Anda mengetahui faktor-faktor lain selain kinerja, silakan balikkan jawaban Anda dengan detail yang cukup.

Jika Anda tidak memahami pertanyaan atau tidak memiliki bukti / sumber yang cukup untuk mendukung jawaban Anda, jangan posting jawaban yang berspekulasi secara luas.


7
siapa yang pernah mendengar komputer deterministik?
sova

1
sebagai litb jawaban yang sangat baik programmers.stackexchange.com/a/99741/192238 menunjukkan, judul dan tubuh pertanyaan ini tampak sedikit serasi: "perilaku terbuka untuk compiler untuk melaksanakan dengan cara mereka sendiri" biasanya disebut sebagai pelaksanaan yang ditetapkan . tentu, sebenarnya UB diizinkan untuk didefinisikan oleh penulis implementasi, tetapi lebih sering daripada tidak, mereka tidak repot-repot (dan mengoptimalkan semuanya, dll.)
underscore_d

Jawaban:


49

Pertama, saya perhatikan bahwa meskipun saya hanya menyebutkan "C" di sini, hal yang sama juga berlaku untuk C ++.

Komentar yang menyebutkan Godel sebagian (tetapi hanya sebagian) tepat sasaran.

Ketika Anda sampai ke sana, perilaku yang tidak terdefinisi dalam standar C sebagian besar hanya menunjukkan batas antara apa yang berusaha didefinisikan oleh standar, dan apa yang tidak.

Teorema Godel (ada dua) pada dasarnya mengatakan bahwa mustahil untuk mendefinisikan sistem matematika yang dapat dibuktikan (dengan aturannya sendiri) menjadi lengkap dan konsisten. Anda dapat membuat aturan Anda sehingga bisa lengkap (kasus yang dia tangani adalah aturan "normal" untuk bilangan asli), atau Anda dapat membuatnya membuktikan konsistensi, tetapi Anda tidak dapat memiliki keduanya.

Dalam hal sesuatu seperti C, yang tidak berlaku secara langsung - sebagian besar, "provabilitas" kelengkapan atau konsistensi sistem bukanlah prioritas tinggi bagi sebagian besar perancang bahasa. Pada saat yang sama, ya, mereka mungkin dipengaruhi (setidaknya sampai taraf tertentu) dengan mengetahui bahwa mustahil untuk mendefinisikan sistem "sempurna" - sistem yang terbukti lengkap dan konsisten. Mengetahui bahwa hal seperti itu tidak mungkin mungkin membuatnya sedikit lebih mudah untuk mundur, bernapas sedikit, dan memutuskan batasan-batasan apa yang akan mereka coba definisikan.

Dengan risiko (lagi-lagi) dituduh sombong, saya akan menganggap standar C sebagai yang diatur (sebagian) oleh dua ide dasar:

  1. Bahasa tersebut harus mendukung berbagai perangkat keras seluas mungkin (idealnya, semua perangkat keras "waras" hingga batas bawah yang wajar).
  2. Bahasa harus mendukung penulisan berbagai perangkat lunak seluas mungkin untuk lingkungan yang diberikan.

Yang pertama berarti bahwa jika seseorang mendefinisikan CPU baru, harus dimungkinkan untuk memberikan implementasi C yang baik, solid, dapat digunakan untuk itu, selama desain jatuh setidaknya cukup dekat dengan beberapa pedoman sederhana - pada dasarnya, jika mengikuti sesuatu pada urutan umum model Von Neumann, dan menyediakan setidaknya sejumlah memori minimum yang masuk akal, yang seharusnya cukup untuk memungkinkan implementasi C. Untuk implementasi "dihosting" (yang dijalankan pada OS) Anda perlu mendukung beberapa gagasan yang sesuai dengan file, dan memiliki set karakter dengan set karakter minimum tertentu (diperlukan 91).

Yang kedua berarti harus mungkin untuk menulis kode yang memanipulasi perangkat keras secara langsung, sehingga Anda dapat menulis hal-hal seperti boot loader, sistem operasi, perangkat lunak tertanam yang berjalan tanpa OS, dll. Pada akhirnya ada beberapa batasan dalam hal ini, sehingga hampir semua sistem praktis operasi, boot loader, dll, mungkin mengandung setidaknya sedikit sedikit kode yang ditulis dalam bahasa assembly. Demikian juga, bahkan sistem tertanam kecil kemungkinan akan menyertakan setidaknya semacam rutin perpustakaan pra-tertulis untuk memberikan akses ke perangkat pada sistem host. Meskipun batas yang tepat sulit untuk didefinisikan, tujuannya adalah bahwa ketergantungan pada kode tersebut harus dijaga agar tetap minimum.

Perilaku tidak terdefinisi dalam bahasa sebagian besar didorong oleh niat untuk bahasa untuk mendukung kemampuan ini. Misalnya, bahasa ini memungkinkan Anda untuk mengkonversi bilangan bulat sembarang menjadi penunjuk, dan mengakses apa pun yang terjadi di alamat itu. Standar tidak berusaha mengatakan apa yang akan terjadi ketika Anda melakukannya (misalnya, bahkan membaca dari beberapa alamat dapat memiliki pengaruh yang terlihat secara eksternal). Pada saat yang sama, tidak ada upaya mencegah Anda dari melakukan hal-hal seperti itu, karena Anda perlu untuk beberapa jenis perangkat lunak yang Anda seharusnya dapat menulis dalam C.

Ada beberapa perilaku tidak terdefinisi yang didorong oleh elemen desain lain juga. Misalnya, satu maksud C lainnya adalah untuk mendukung kompilasi terpisah. Ini berarti (misalnya) bahwa ini dimaksudkan agar Anda dapat "menautkan" potongan-potongan menggunakan tautan yang kira-kira mengikuti apa yang sebagian besar dari kita lihat sebagai model tautan biasa. Secara khusus, harus dimungkinkan untuk menggabungkan modul yang dikompilasi secara terpisah ke dalam program yang lengkap tanpa sepengetahuan semantik bahasa.

Ada tipe lain dari perilaku tidak terdefinisi (yang jauh lebih umum di C ++ daripada C), yang hadir hanya karena batasan pada teknologi kompiler - hal-hal yang pada dasarnya kita tahu adalah kesalahan, dan mungkin ingin kompiler mendiagnosis sebagai kesalahan, tetapi mengingat batas saat ini pada teknologi kompiler, diragukan bahwa mereka dapat didiagnosis dalam semua keadaan. Banyak dari ini didorong oleh persyaratan lain, seperti untuk kompilasi terpisah, sehingga sebagian besar masalah keseimbangan persyaratan yang saling bertentangan, dalam hal ini panitia umumnya memilih untuk mendukung kemampuan yang lebih besar, bahkan jika itu berarti kurangnya mendiagnosis beberapa masalah yang mungkin terjadi, daripada membatasi kemampuan untuk memastikan bahwa semua masalah yang mungkin didiagnosis.

Perbedaan-perbedaan ini dalam niat mendorong sebagian besar perbedaan antara C dan sesuatu seperti Java atau sistem berbasis CLI Microsoft. Yang terakhir ini secara eksplisit terbatas untuk bekerja dengan perangkat keras yang jauh lebih terbatas, atau membutuhkan perangkat lunak untuk meniru perangkat keras yang lebih spesifik yang mereka targetkan. Mereka juga secara khusus berniat untuk mencegah manipulasi langsung perangkat keras, alih-alih mengharuskan Anda menggunakan sesuatu seperti JNI atau P / Invoke (dan kode yang ditulis dalam sesuatu seperti C) untuk melakukan upaya semacam itu.

Kembali ke teorema Godel sejenak, kita dapat menggambar sesuatu yang paralel: Java dan CLI telah memilih alternatif "konsisten secara internal", sementara C telah memilih alternatif "lengkap". Tentu saja, ini analogi yang sangat kasar - saya ragu ada orang yang mencoba bukti formal baik konsistensi internal atau kelengkapan dalam kedua kasus. Meskipun demikian, gagasan umum tidak cukup cocok dengan pilihan yang telah mereka ambil.


25
Saya pikir Teorema Godel adalah ikan haring merah. Mereka berurusan dengan membuktikan suatu sistem dari aksioma sendiri, yang tidak terjadi di sini: C tidak perlu ditentukan dalam C. Sangat mungkin untuk memiliki bahasa yang sepenuhnya ditentukan (pertimbangkan mesin Turing).
poolie

9
Maaf, tapi saya khawatir Anda benar-benar salah memahami teorema Godel. Mereka berurusan dengan ketidakmungkinan untuk membuktikan semua pernyataan yang benar dalam sistem logika yang konsisten; dalam hal komputasi, teorema ketidaklengkapan ini analog dengan mengatakan bahwa ada masalah yang tidak dapat dipecahkan oleh program apa pun - masalah dianalogikan dengan pernyataan yang benar, program untuk bukti dan model perhitungan untuk sistem logika. Ini tidak memiliki koneksi sama sekali dengan perilaku yang tidak terdefinisi. Lihat penjelasan analoginya di sini: scottaaronson.com/blog/?p=710 .
Alex ten Brink

5
Saya harus mencatat bahwa mesin Von Neumann tidak diperlukan untuk implementasi C. Sangat mungkin (dan bahkan tidak terlalu sulit) untuk mengembangkan implementasi C untuk arsitektur Harvard (dan saya tidak akan terkejut melihat banyak implementasi seperti itu pada sistem embedded)
bdonlan

1
Sayangnya, filsafat kompiler C modern membawa UB ke tingkat yang sama sekali baru. Bahkan dalam kasus-kasus di mana sebuah program dipersiapkan untuk menangani hampir semua konsekuensi "alami" yang masuk akal dari bentuk tertentu dari Perilaku Tidak Terdefinisi, dan mereka yang tidak dapat mengatasinya paling tidak akan dikenali (misalnya limpahan bilangan bulat yang terperangkap), filosofi baru ini berpihak pada mem-bypass kode apa pun yang tidak dapat dieksekusi kecuali UB akan terjadi, mengubah kode yang akan berperilaku benar pada sebagian besar implementasi menjadi kode yang "lebih efisien" tetapi hanya salah.
supercat

20

Alasan C menjelaskan

Istilah perilaku yang tidak ditentukan, perilaku yang tidak terdefinisi, dan perilaku yang ditentukan implementasi digunakan untuk mengkategorikan hasil dari program penulisan yang sifat-sifatnya tidak dijelaskan atau tidak dapat sepenuhnya dijelaskan oleh standar. Tujuan mengadopsi kategorisasi ini adalah untuk memungkinkan variasi tertentu di antara implementasi yang memungkinkan kualitas implementasi menjadi kekuatan aktif di pasar serta untuk memungkinkan ekstensi populer tertentu , tanpa menghapus cap kesesuaian dengan Standar. Lampiran F to the Standard mengelompokkan perilaku yang termasuk dalam salah satu dari tiga kategori ini.

Perilaku yang tidak ditentukan memberikan keleluasaan pada implementor dalam menerjemahkan program. Garis lintang ini tidak sampai gagal menerjemahkan program.

Perilaku tidak terdefinisi memberikan lisensi implementor untuk tidak menangkap kesalahan program tertentu yang sulit didiagnosis. Ini juga mengidentifikasi bidang-bidang yang kemungkinan dapat disesuaikan dengan ekstensi bahasa: implementor dapat menambah bahasa dengan memberikan definisi tentang perilaku yang tidak terdefinisi secara resmi.

Perilaku yang ditentukan implementasi memberikan pelaksana kebebasan untuk memilih pendekatan yang sesuai, tetapi mengharuskan pilihan ini dijelaskan kepada pengguna. Perilaku yang ditetapkan sebagai implementasi yang didefinisikan umumnya adalah perilaku di mana pengguna dapat membuat keputusan pengkodean yang bermakna berdasarkan definisi implementasi. Para pelaksana harus memperhatikan kriteria ini ketika memutuskan seberapa luas definisi implementasi seharusnya. Seperti halnya perilaku yang tidak ditentukan, gagal menerjemahkan sumber yang mengandung perilaku yang ditentukan implementasi bukan respons yang memadai.

Penting juga manfaat untuk program, tidak hanya manfaat untuk implementasi. Suatu program yang bergantung pada perilaku tidak terdefinisi masih dapat menyesuaikan , jika itu diterima oleh implementasi yang sesuai. Adanya perilaku yang tidak terdefinisi memungkinkan suatu program untuk menggunakan fitur-fitur non-portabel yang secara eksplisit ditandai seperti itu ("perilaku tidak terdefinisi"), tanpa menjadi tidak sesuai. Catatan rasionalnya:

Kode C bisa non-portabel. Meskipun berusaha untuk memberi para programmer kesempatan untuk menulis program yang benar-benar portabel, Komite tidak ingin memaksa programmer untuk menulis secara portabel, untuk mencegah penggunaan C sebagai `` assembler tingkat tinggi '': kemampuan untuk menulis khusus mesin kode adalah salah satu kekuatan C. Ini adalah prinsip ini yang sebagian besar memotivasi menggambar perbedaan antara program yang benar - benar sesuai dan program yang sesuai (§1.7).

Dan pada 1,7 dicatat

Definisi kepatuhan tiga kali lipat digunakan untuk memperluas populasi program yang sesuai dan membedakan antara program yang sesuai menggunakan implementasi tunggal dan program penyesuaian portabel.

Program yang sangat sesuai adalah istilah lain untuk program portabel yang maksimal. Tujuannya adalah untuk memberi programmer kesempatan berjuang untuk membuat program C yang kuat yang juga sangat portabel, tanpa merendahkan program C yang sangat berguna yang kebetulan tidak menjadi portabel. Demikian kata keterangan secara ketat.

Dengan demikian, program kotor kecil ini yang berfungsi dengan baik pada GCC masih sesuai !


15

Masalah kecepatan terutama masalah bila dibandingkan dengan C. Jika C ++ melakukan beberapa hal yang mungkin masuk akal, seperti menginisialisasi array besar tipe primitif, itu akan kehilangan satu ton tolok ukur untuk kode C. Jadi C ++ menginisialisasi tipe datanya sendiri, tetapi membiarkan tipe C seperti sebelumnya.

Perilaku tidak terdefinisi lainnya hanya mencerminkan kenyataan. Salah satu contoh adalah bit-shifting dengan jumlah yang lebih besar dari tipe. Itu sebenarnya berbeda antara generasi perangkat keras dari keluarga yang sama. Jika Anda memiliki aplikasi 16-bit, biner yang sama persis akan memberikan hasil yang berbeda pada 80286 dan 80386. Jadi standar bahasa mengatakan bahwa kita tidak tahu!

Beberapa hal dipertahankan seperti semula, seperti urutan evaluasi subekspresi yang tidak ditentukan. Awalnya ini diyakini membantu kompiler mengoptimalkan penulis dengan lebih baik. Saat ini kompiler cukup baik untuk mengetahuinya, tetapi biaya untuk menemukan semua tempat di kompiler yang ada yang memanfaatkan kebebasan terlalu tinggi.


+1 untuk paragraf kedua, yang menunjukkan sesuatu yang aneh untuk ditetapkan sebagai perilaku yang ditentukan implementasi.
David Thornley

3
Bit bergeser hanya sebuah contoh menerima perilaku compiler yang tidak terdefinisi dan menggunakan kapabilit perangkat keras. Akan sepele untuk menentukan hasil C untuk sedikit pergeseran ketika jumlah lebih besar dari jenis, tetapi mahal untuk diterapkan pada beberapa perangkat keras.
mattnz

7

Sebagai salah satu contoh, akses pointer hampir tidak dapat ditentukan dan tidak harus hanya untuk alasan kinerja. Misalnya, pada beberapa sistem, memuat register spesifik dengan pointer akan menghasilkan pengecualian perangkat keras. Pada SPARC mengakses objek memori yang tidak selaras akan menyebabkan kesalahan bus, tetapi pada x86 itu akan "hanya" menjadi lambat. Sangat sulit untuk benar-benar menentukan perilaku dalam kasus-kasus tersebut karena perangkat keras yang mendikte menentukan apa yang akan terjadi, dan C ++ bersifat portabel untuk banyak jenis perangkat keras.

Tentu saja itu juga memberikan kebebasan kompiler untuk menggunakan pengetahuan khusus arsitektur. Untuk contoh perilaku yang tidak ditentukan, pergeseran kanan dari nilai yang ditandatangani mungkin logis atau aritmatika tergantung pada perangkat keras yang mendasarinya, untuk memungkinkan penggunaan operasi shift mana saja yang tersedia dan tidak memaksakan emulasi perangkat lunak terhadapnya.

Saya percaya itu juga membuat pekerjaan kompiler-penulis lebih mudah tetapi saya tidak dapat mengingat contohnya sekarang. Saya akan menambahkannya jika saya mengingat situasinya.


3
Bahasa C dapat ditentukan sedemikian rupa sehingga selalu harus menggunakan bacaan byte-by-byte pada sistem dengan pembatasan penyelarasan, dan sedemikian rupa sehingga harus memberikan perangkap pengecualian dengan perilaku yang didefinisikan dengan baik untuk akses alamat yang tidak valid. Tetapi tentu saja ini semua akan sangat mahal (dalam ukuran kode, kompleksitas, dan kinerja) dan tidak akan menawarkan manfaat apa pun untuk waras, kode yang benar.
R ..

6

Sederhana: Kecepatan, dan portabilitas. Jika C ++ menjamin bahwa Anda mendapat pengecualian saat Anda membatalkan referensi pointer yang tidak valid, maka itu tidak akan portabel untuk perangkat keras yang disematkan. Jika C ++ dijamin beberapa hal lain seperti primitif selalu diinisialisasi, maka itu akan lebih lambat, dan pada saat asal C ++, lebih lambat adalah hal yang benar-benar buruk.


1
Hah? Apa hubungan pengecualian dengan perangkat keras tertanam?
Mason Wheeler

2
Pengecualian dapat mengunci sistem dengan cara yang sangat buruk untuk Sistem Tertanam yang perlu merespons dengan cepat. Ada situasi di mana pembacaan yang salah jauh lebih merusak daripada sistem yang melambat.
Insinyur Dunia

1
@ Alasan: Karena perangkat keras harus menangkap akses yang tidak valid. Sangat mudah bagi Windows untuk melakukan pelanggaran akses, dan lebih sulit untuk perangkat keras tertanam tanpa sistem operasi untuk melakukan apa pun kecuali mati.
DeadMG

3
Juga ingat bahwa tidak setiap CPU memiliki MMU untuk melindungi dari akses yang tidak valid pada perangkat keras. Jika Anda mulai membutuhkan bahasa Anda untuk memeriksa semua akses pointer, maka Anda harus meniru MMU pada CPU tanpa CPU - dan karenanya SETIAP akses memori menjadi sangat mahal.
lembut

4

C ditemukan pada mesin dengan byte 9bit dan tanpa unit floating point - anggaplah ia mengamanatkan bahwa byte adalah 9bits, kata 18bits dan float harus diimplementasikan menggunakan pra-IEEE754 aritmatic?


5
Saya menduga Anda memikirkan Unix - C pada awalnya digunakan pada PDP-11, yang sebenarnya standar saat ini cukup konvensional. Saya pikir ide dasarnya tetap ada.
Jerry Coffin

@ Jerry - ya, Anda benar - saya semakin tua!
Martin Beckett

Yup - kebetulan yang terbaik dari kita, aku takut.
Jerry Coffin

4

Saya tidak berpikir alasan pertama untuk UB adalah untuk memberikan ruang bagi kompiler untuk mengoptimalkan, tetapi hanya kemungkinan untuk menggunakan implementasi yang jelas untuk target pada saat arsitektur memiliki lebih banyak variasi daripada sekarang (ingat jika C dirancang pada suatu PDP-11 yang memiliki arsitektur yang agak akrab, port pertama adalah ke Honeywell 635 yang jauh kurang dikenal - kata addressable, menggunakan kata-kata 36 bit, 6 atau 9 bit byte, alamat 18 bit ... well setidaknya itu digunakan 2's melengkapi). Tetapi jika optimasi berat bukan target, implementasi yang jelas tidak termasuk menambahkan run-time check untuk overflow, shift menghitung lebih dari ukuran register, yang alias dalam ekspresi memodifikasi beberapa nilai.

Hal lain yang diperhitungkan adalah kemudahan implementasi. Kompiler AC pada saat itu adalah beberapa lintasan menggunakan beberapa proses karena memiliki satu proses menangani semuanya tidak akan mungkin (program akan terlalu besar). Meminta pemeriksaan koherensi yang berat tidak memungkinkan - terutama ketika melibatkan beberapa CU. (Program lain selain kompiler C, lint, digunakan untuk itu).


Saya bertanya-tanya apa yang mendorong filosofi UB yang berubah dari "Izinkan pemrogram menggunakan perilaku yang diekspos oleh platform mereka" hingga "Cari alasan untuk membiarkan kompiler menerapkan perilaku yang benar-benar aneh"? Saya juga bertanya-tanya berapa optimasi seperti itu akhirnya meningkatkan ukuran kode setelah kode dimodifikasi untuk bekerja di bawah kompiler baru? Saya tidak akan terkejut jika dalam banyak kasus satu-satunya efek menambahkan "optimasi" seperti itu ke kompiler adalah untuk memaksa programmer untuk menulis kode yang lebih besar dan lebih lambat sehingga menghindari kompiler mematahkannya.
supercat

Ini adalah penyimpangan dalam POV. Orang menjadi kurang sadar akan mesin tempat program mereka berjalan, mereka menjadi lebih peduli dengan portabilitas sehingga mereka terhindar dari bergantung pada perilaku yang tidak ditentukan, tidak ditentukan dan implementasi yang didefinisikan. Ada tekanan pada pengoptimal untuk mendapatkan hasil terbaik pada benchmark, dan itu berarti memanfaatkan setiap keringanan hukuman yang ditinggalkan oleh spesifikasi bahasa. Ada juga fakta bahwa Internet - Usenet pada suatu waktu, SE saat ini - pengacara bahasa juga cenderung memberikan pandangan yang bias tentang dasar pemikiran dan perilaku penulis kompiler.
Pemrogram

1
Yang membuat saya penasaran adalah pernyataan yang saya lihat sebagai efek dari "C mengasumsikan bahwa programmer tidak akan pernah terlibat dalam perilaku yang tidak terdefinisi" - sebuah fakta yang secara historis tidak benar. Pernyataan yang benar adalah "C berasumsi bahwa pemrogram tidak akan memicu perilaku yang tidak ditentukan oleh standar kecuali jika disiapkan untuk berurusan dengan konsekuensi platform alami dari perilaku itu. Mengingat bahwa C dirancang sebagai bahasa pemrograman sistem, sebagian besar tujuannya adalah untuk memungkinkan pemrogram untuk melakukan hal-hal khusus sistem yang tidak didefinisikan oleh standar bahasa; gagasan bahwa mereka tidak akan pernah melakukannya adalah tidak masuk akal
supercat

Adalah baik bagi programmer untuk melakukan upaya ekstra untuk memastikan portabilitas dalam kasus di mana platform yang berbeda secara inheren melakukan hal yang berbeda , tetapi penulis kompiler membuang waktu semua orang ketika mereka menghilangkan perilaku yang secara historis dapat diprediksi oleh programmer secara umum pada semua kompiler di masa depan. Diberikan bilangan bulat idan n, sedemikian sehingga n < INT_BITSdan i*(1<<n)tidak akan meluap, saya akan mempertimbangkan i<<=n;lebih jelas dari i=(unsigned)i << n;; pada banyak platform akan lebih cepat dan lebih kecil dari i*=(1<<N);. Apa yang didapat dari kompiler yang melarangnya?
supercat

Meskipun saya pikir itu akan baik untuk standar untuk memungkinkan jebakan untuk banyak hal yang disebut UB (misalnya integer overflow), dan ada alasan bagus untuk itu tidak mengharuskan perangkap melakukan apa pun yang dapat diprediksi, saya akan berpikir bahwa dari setiap sudut pandang dapat dibayangkan standar akan ditingkatkan jika diperlukan bahwa sebagian besar bentuk UB harus menghasilkan nilai yang tidak ditentukan atau mendokumentasikan fakta bahwa mereka berhak untuk melakukan sesuatu yang lain, tanpa diharuskan untuk mendokumentasikan apa yang mungkin menjadi sesuatu yang lain. Kompiler yang menjadikan semuanya "UB" akan sah, tetapi kemungkinan tidak disukai ...
supercat

3

Salah satu kasus klasik awal ditandatangani tambahan bilangan bulat. Pada beberapa prosesor yang digunakan, itu akan menyebabkan kesalahan, dan yang lain hanya akan melanjutkan dengan nilai (kemungkinan nilai modular yang sesuai). Menentukan kedua kasus akan berarti bahwa program untuk mesin dengan gaya aritmatika yang tidak disukai harus memiliki kode tambahan, termasuk cabang bersyarat, untuk sesuatu yang sama seperti penambahan bilangan bulat.


Penambahan integer adalah kasus yang menarik; di luar kemungkinan perilaku jebakan yang dalam beberapa kasus akan berguna tetapi dalam kasus lain dapat menyebabkan eksekusi kode acak, ada situasi di mana masuk akal bagi kompiler untuk membuat kesimpulan berdasarkan fakta bahwa integer overflow tidak ditentukan untuk dibungkus. Sebagai contoh, kompiler di mana int16 bit dan tanda-perpanjangan shift mahal bisa menghitung (uchar1*uchar2) >> 4menggunakan pergeseran non-tanda-diperpanjang. Sayangnya, beberapa kompiler memperluas inferensi tidak hanya pada hasil, tetapi pada operan.
supercat

2

Saya akan mengatakan itu kurang tentang filsafat daripada tentang kenyataan - C selalu menjadi bahasa lintas platform, dan standar harus mencerminkan itu dan fakta bahwa pada saat standar apa pun dirilis, akan ada sejumlah besar implementasi pada banyak perangkat keras yang berbeda. Suatu standar yang melarang perilaku yang diperlukan akan diabaikan atau menghasilkan badan standar yang bersaing.


Awalnya, banyak perilaku dibiarkan tidak terdefinisi untuk memungkinkan kemungkinan bahwa sistem yang berbeda akan melakukan hal yang berbeda, termasuk memicu jebakan perangkat keras dengan handler yang mungkin atau mungkin tidak dapat dikonfigurasi (dan mungkin, jika tidak dikonfigurasi, menyebabkan perilaku sewenang-wenang yang tidak dapat diprediksi). Mengharuskan pergeseran kiri dari nilai negatif tidak menjebak, misalnya, akan memecah kode apa pun yang dirancang untuk sistem di mana ia melakukannya dan mengandalkan perilaku tersebut. Singkatnya, mereka dibiarkan tidak terdefinisi agar tidak mencegah pelaksana memberikan perilaku yang mereka pikir berguna .
supercat

Sayangnya, bagaimanapun, yang telah diputar sehingga kode yang tahu bahwa itu berjalan pada prosesor yang akan melakukan sesuatu yang berguna dalam kasus tertentu tidak dapat mengambil keuntungan dari perilaku seperti itu, karena kompiler dapat menggunakan fakta bahwa standar C tidak dapat menentukan perilaku (meskipun platform akan) untuk menerapkan penulisan ulang dunia bizarro ke kode.
supercat

1

Beberapa perilaku tidak dapat didefinisikan dengan cara apa pun yang masuk akal. Maksud saya mengakses pointer yang dihapus. Satu-satunya cara untuk mendeteksinya adalah melarang nilai pointer setelah penghapusan (menghafal nilainya di suatu tempat dan tidak mengizinkan fungsi alokasi mengembalikannya lagi). Tidak hanya menghafal seperti itu akan berlebihan, tetapi untuk program yang berjalan lama akan menyebabkan kehabisan nilai pointer yang diizinkan.


atau Anda dapat mengalokasikan semua pointer sebagai weak_ptrdan membatalkan semua referensi ke pointer yang mendapat delete... oh tunggu, kami sedang mendekati pengumpulan sampah: /
Matthieu M.

boost::weak_ptrImplementasi adalah template yang cukup bagus untuk memulai dengan pola penggunaan ini. Daripada melacak dan meniadakan secara weak_ptrseksternal, yang weak_ptradil berkontribusi pada shared_ptrhitung lemah, dan hitung lemah pada dasarnya adalah penghitungan ulang ke penunjuk itu sendiri. Dengan demikian, Anda dapat membatalkan shared_ptrtanpa harus segera menghapusnya. Itu tidak sempurna (Anda masih dapat memiliki banyak kadaluarsa weak_ptrmempertahankan yang mendasarinya shared_counttanpa alasan yang baik) tetapi setidaknya itu cepat dan efisien.
lembut

0

Saya akan memberi Anda sebuah contoh di mana hampir tidak ada pilihan yang masuk akal selain perilaku yang tidak terdefinisi. Pada prinsipnya, pointer apa pun dapat menunjuk ke memori yang mengandung variabel apa pun, dengan pengecualian kecil variabel lokal yang diketahui kompiler tidak pernah diambil alamatnya. Namun, untuk mendapatkan kinerja yang dapat diterima pada CPU modern, kompiler harus menyalin nilai variabel ke register. Mengoperasikan sepenuhnya dari memori adalah non-starter.

Ini pada dasarnya memberi Anda dua pilihan:

1) Buang semuanya keluar dari register sebelum akses apa pun melalui pointer, kalau-kalau pointer menunjuk ke memori variabel tertentu itu. Kemudian muat semua yang diperlukan kembali ke register, kalau-kalau nilai-nilai diubah melalui pointer.

2) Memiliki seperangkat aturan untuk kapan pointer diizinkan untuk alias variabel dan ketika kompiler diizinkan untuk menganggap bahwa pointer tidak alias variabel.

C memilih opsi 2, karena 1 akan mengerikan untuk kinerja. Tapi kemudian, apa yang terjadi jika pointer alias variabel dengan cara aturan C melarang? Karena efeknya tergantung pada apakah kompilator memang menyimpan variabel dalam register, tidak ada cara bagi standar C untuk secara definitif menjamin hasil spesifik.


Akan ada perbedaan semantik antara mengatakan "Kompiler diperbolehkan berperilaku seolah-olah X benar" dan mengatakan "Setiap program di mana X tidak benar akan terlibat dalam Perilaku Tidak Terdefinisi", meskipun sayangnya standar untuk tidak membuat perbedaan menjadi jelas. Dalam banyak situasi, termasuk contoh aliasing Anda, pernyataan sebelumnya akan memungkinkan banyak optimisasi kompiler yang tidak mungkin dilakukan sebaliknya; yang terakhir memungkinkan beberapa "optimasi", tetapi banyak dari optimasi yang terakhir adalah hal-hal yang tidak diinginkan oleh programmer.
supercat

Misalnya, jika beberapa kode menetapkan a fooke 42, dan kemudian memanggil metode yang menggunakan pointer yang dimodifikasi secara tidak sah untuk diatur fooke 44, saya dapat melihat manfaat untuk mengatakan bahwa sampai penulisan "sah" berikutnya foo, upaya untuk membacanya mungkin sah menghasilkan 42 atau 44, dan ekspresi seperti foo+foobahkan bisa menghasilkan 86, tapi saya melihat jauh lebih sedikit manfaat untuk memungkinkan kompiler membuat kesimpulan diperpanjang dan bahkan retroaktif, mengubah Perilaku Tidak Terdefinisi yang perilaku "alami" yang masuk akal semuanya akan menjadi jinak, menjadi lisensi untuk menghasilkan kode yang tidak masuk akal.
supercat

0

Secara historis, Perilaku Tidak Terdefinisi memiliki dua tujuan utama:

  1. Untuk menghindari mengharuskan penulis kompiler untuk menghasilkan kode untuk menangani kondisi yang seharusnya tidak pernah terjadi.

  2. Untuk memungkinkan kemungkinan bahwa tanpa adanya kode untuk secara eksplisit menangani kondisi seperti itu, implementasi dapat memiliki berbagai jenis perilaku "alami" yang, dalam beberapa kasus, akan berguna.

Sebagai contoh sederhana, pada beberapa platform perangkat keras, mencoba untuk menambahkan bersama dua bilangan bulat bertanda positif yang jumlahnya terlalu besar untuk masuk dalam bilangan bulat yang ditandatangani akan menghasilkan bilangan bulat bertanda negatif tertentu. Pada implementasi lain akan memicu jebakan prosesor. Untuk standar C untuk mengamanatkan perilaku mana pun akan memerlukan bahwa penyusun untuk platform yang perilaku alami berbeda dari standar harus menghasilkan kode tambahan untuk menghasilkan perilaku yang benar - kode yang mungkin lebih mahal daripada kode untuk melakukan penambahan yang sebenarnya. Lebih buruk lagi, itu berarti bahwa programmer yang menginginkan perilaku "alami" harus menambahkan lebih banyak kode tambahan untuk mencapainya (dan bahwa kode tambahan akan lebih mahal daripada penambahan).

Sayangnya, beberapa penulis kompilator telah mengambil filosofi bahwa penyusun harus pergi keluar dari jalan mereka untuk menemukan kondisi yang akan membangkitkan Perilaku Tidak Terdefinisi dan, dengan anggapan bahwa situasi seperti itu mungkin tidak pernah terjadi, menarik kesimpulan panjang dari itu. Jadi, pada sistem dengan 32-bit int, diberikan kode seperti:

uint32_t foo(uint16_t q, int *p)
{
  if (q > 46340)
    *p++;
  return q*q;
}

standar C akan memungkinkan kompiler untuk mengatakan bahwa jika q adalah 46341 atau lebih besar, ekspresi q * q akan menghasilkan hasil yang terlalu besar untuk ditampung dalam int, akibatnya menyebabkan Perilaku tidak terdefinisi, dan sebagai akibatnya kompiler akan berhak untuk menganggap bahwa tidak dapat terjadi dan dengan demikian tidak akan diperlukan kenaikan *pjika itu terjadi. Jika kode panggilan digunakan *psebagai indikator bahwa ia harus membuang hasil perhitungan, efek dari optimasi mungkin untuk mengambil kode yang akan menghasilkan hasil yang masuk akal pada sistem yang melakukan hampir semua cara yang bisa dibayangkan dengan bilangan bulat bilangan bulat (perangkap mungkin jelek, tapi setidaknya masuk akal), dan mengubahnya menjadi kode yang mungkin berperilaku tidak masuk akal.


-6

Efisiensi adalah alasan yang biasa, tetapi apa pun alasannya, perilaku yang tidak terdefinisi adalah ide yang buruk untuk portabilitas. Akibatnya, perilaku yang tidak terdefinisi menjadi asumsi yang tidak diverifikasi dan tidak dinyatakan.


7
OP menetapkan ini: "Pertanyaan saya bukan tentang apa perilaku tidak terdefinisi itu, atau apakah itu benar-benar buruk. Saya tahu bahaya dan sebagian besar kutipan perilaku tidak terdefinisi yang relevan dari standar, jadi tolong jangan posting jawaban tentang seberapa buruk itu. . " Sepertinya Anda tidak membaca pertanyaan.
Etienne de Martel
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.