Dapatkah cabang dengan perilaku tidak terdefinisi diasumsikan tidak dapat dijangkau dan dioptimalkan sebagai kode mati?

89

Perhatikan pernyataan berikut:

*((char*)NULL) = 0; //undefined behavior

Ini jelas memunculkan perilaku yang tidak terdefinisi. Apakah adanya pernyataan seperti itu dalam program tertentu berarti bahwa keseluruhan program tidak terdefinisi atau bahwa perilaku hanya menjadi tidak terdefinisi begitu aliran kendali mencapai pernyataan ini?

Apakah program berikut akan terdefinisi dengan baik jika pengguna tidak pernah memasukkan nomor 3?

while (true) {
 int num = ReadNumberFromConsole();
 if (num == 3)
  *((char*)NULL) = 0; //undefined behavior
}

Atau apakah itu perilaku yang sepenuhnya tidak terdefinisi, apa pun yang dimasukkan pengguna?

Selain itu, dapatkah kompilator berasumsi bahwa perilaku tidak terdefinisi tidak akan pernah dijalankan saat runtime? Itu akan memungkinkan untuk berpikir mundur dalam waktu:

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Di sini, kompilator dapat beralasan bahwa jika num == 3kita akan selalu memanggil perilaku tidak terdefinisi. Oleh karena itu, kasus ini tidak mungkin dilakukan dan nomornya tidak perlu dicetak. Seluruh ifpernyataan bisa dioptimalkan. Apakah penalaran mundur semacam ini diperbolehkan menurut standar?

usr
sumber
19
terkadang saya bertanya-tanya apakah pengguna dengan banyak perwakilan mendapatkan lebih banyak suara positif pada pertanyaan karena "oh mereka memiliki banyak reputasi, ini pasti pertanyaan yang bagus" ... tetapi dalam kasus ini saya membaca pertanyaan dan berpikir "wow, ini bagus "bahkan sebelum aku melihat penanya.
turbulencetoo
4
Saya pikir saat munculnya perilaku tidak terdefinisi, tidak terdefinisi.
eerorika
6
Standar C ++ secara eksplisit mengatakan bahwa jalur eksekusi dengan perilaku yang tidak ditentukan pada titik mana pun benar-benar tidak ditentukan. Saya bahkan akan menafsirkannya sebagai mengatakan bahwa program apa pun dengan perilaku tidak terdefinisi di jalur benar-benar tidak terdefinisi (yang mencakup hasil yang wajar di bagian lain, tetapi itu tidak dijamin). Kompiler bebas menggunakan perilaku tidak terdefinisi untuk memodifikasi program Anda. blog.llvm.org/2011/05/what-every-c-programmer-should-know.html berisi beberapa contoh bagus.
Jens
4
@ Jens: Ini benar-benar berarti hanya jalur eksekusi. Jika tidak, masalah sudah berakhir const int i = 0; if (i) 5/i;.
MSalters
1
Kompiler secara umum tidak dapat membuktikan bahwa PrintToConsoletidak memanggil std::exitsehingga harus melakukan panggilan.
MSalters

Jawaban:

66

Apakah adanya pernyataan seperti itu dalam program tertentu berarti bahwa keseluruhan program tidak terdefinisi atau bahwa perilaku hanya menjadi tidak terdefinisi begitu aliran kendali mencapai pernyataan ini?

Tidak keduanya. Kondisi pertama terlalu kuat dan yang kedua terlalu lemah.

Akses objek terkadang diurutkan, tetapi standarnya menggambarkan perilaku program di luar waktu. Danvil sudah mengutip:

jika eksekusi semacam itu mengandung operasi yang tidak ditentukan, Standar Internasional ini tidak mensyaratkan pelaksanaan yang menjalankan program itu dengan masukan itu (bahkan tidak berkaitan dengan operasi sebelum operasi pertama yang tidak ditentukan)

Ini dapat diartikan:

Jika eksekusi program menghasilkan perilaku tidak terdefinisi, maka seluruh program memiliki perilaku tak terdefinisi.

Jadi, unreachable statement dengan UB tidak berarti memberi UB program. Pernyataan yang terjangkau (karena nilai masukan) tidak pernah tercapai, tidak memberikan program UB. Itu sebabnya kondisi pertamamu terlalu kuat.

Sekarang, penyusun secara umum tidak bisa membedakan apa yang memiliki UB. Jadi untuk memungkinkan pengoptimal untuk mengatur ulang pernyataan dengan UB potensial yang akan diatur ulang jika perilakunya ditentukan, perlu untuk mengizinkan UB untuk "mencapai waktu kembali" dan salah sebelum titik urutan sebelumnya (atau di C ++ 11, bagi UB untuk mempengaruhi hal-hal yang diurutkan sebelum hal UB). Karena itu kondisi keduamu terlalu lemah.

Contoh utamanya adalah ketika pengoptimal mengandalkan aliasing yang ketat. Inti dari aturan aliasing yang ketat adalah untuk memungkinkan compiler untuk menata ulang operasi yang tidak dapat diurutkan ulang secara valid jika mungkin pointer yang dimaksud alias memori yang sama. Jadi jika Anda menggunakan penunjuk aliasing secara ilegal, dan UB memang terjadi, maka itu dapat dengan mudah memengaruhi pernyataan "sebelum" pernyataan UB. Sejauh menyangkut mesin abstrak, pernyataan UB belum dijalankan. Sejauh menyangkut kode objek yang sebenarnya, itu telah dijalankan sebagian atau seluruhnya. Namun standar tersebut tidak mencoba merinci apa artinya bagi pengoptimal untuk menyusun ulang pernyataan, atau apa implikasinya bagi UB. Itu hanya memberi lisensi implementasi untuk melakukan kesalahan segera setelah diinginkan.

Anda bisa menganggapnya sebagai, "UB memiliki mesin waktu".

Secara khusus untuk menjawab contoh Anda:

  • Perilaku hanya tidak ditentukan jika 3 dibaca.
  • Penyusun dapat dan memang menghilangkan kode sebagai mati jika blok dasar berisi operasi tertentu yang tidak ditentukan. Mereka diizinkan (dan saya kira begitu) dalam kasus-kasus yang bukan merupakan blok dasar tetapi di mana semua cabang mengarah ke UB. Contoh ini bukanlah kandidat kecuali PrintToConsole(3)diketahui pasti akan kembali. Itu bisa menimbulkan pengecualian atau apa pun.

Contoh serupa untuk yang kedua adalah opsi gcc -fdelete-null-pointer-checks, yang dapat mengambil kode seperti ini (saya belum memeriksa contoh khusus ini, anggap itu ilustrasi dari gagasan umum):

void foo(int *p) {
    if (p) *p = 3;
    std::cout << *p << '\n';
}

dan ubah menjadi:

*p = 3;
std::cout << "3\n";

Mengapa? Karena jika pnull maka kodenya memiliki UB, jadi kompilator mungkin menganggapnya bukan null dan mengoptimalkannya. Kernel linux tersandung ini ( https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2009-1897 ) pada dasarnya karena beroperasi dalam mode di mana dereferensi pointer nol tidak seharusnya menjadi UB, diharapkan menghasilkan pengecualian perangkat keras yang ditentukan yang dapat ditangani kernel. Saat pengoptimalan diaktifkan, gcc memerlukan penggunaan -fno-delete-null-pointer-checksuntuk untuk memberikan jaminan yang melebihi standar.

PS Jawaban praktis untuk pertanyaan "kapan perilaku yang tidak terdefinisi menyerang?" adalah "10 menit sebelum Anda berencana berangkat hari itu".

Steve Jessop
sumber
4
Sebenarnya, ada beberapa masalah keamanan karena hal ini di masa lalu. Secara khusus, setiap pemeriksaan overflow setelah fakta berada dalam bahaya dioptimalkan karena hal ini. Sebagai contoh void can_add(int x) { if (x + 100 < x) complain(); }dapat dioptimalkan pergi seluruhnya, karena jika x+100 doesn' meluap tidak ada yang terjadi, dan jika x+100 tidak meluap, itu UB sesuai standar, sehingga tidak ada mungkin terjadi.
fgp
3
@fgp: benar, itu adalah pengoptimalan yang dikeluhkan orang dengan getir jika mereka tersandung, karena mulai terasa seperti kompilator dengan sengaja melanggar kode Anda untuk menghukum Anda. "Mengapa saya menulis seperti itu jika saya ingin Anda menghapusnya!" ;-) Namun menurut saya terkadang berguna bagi pengoptimal saat memanipulasi ekspresi aritmatika yang lebih besar, untuk mengasumsikan tidak ada luapan dan menghindari sesuatu yang mahal yang hanya diperlukan dalam kasus tersebut.
Steve Jessop
2
Apakah benar untuk mengatakan bahwa program tidak ditentukan jika pengguna tidak pernah memasukkan 3, tetapi jika ia memasukkan 3 selama eksekusi, seluruh eksekusi menjadi tidak ditentukan? Segera setelah 100% yakin bahwa program akan memunculkan perilaku tidak terdefinisi (dan tidak lebih cepat dari itu) perilaku diizinkan menjadi apa saja. Apakah pernyataan saya ini 100% benar?
usr
3
@usr: Saya yakin itu benar, ya. Dengan contoh khusus Anda (dan membuat beberapa asumsi tentang keniscayaan data yang sedang diproses), saya pikir implementasi pada prinsipnya dapat melihat ke depan dalam buffered STDIN untuk 3jika diinginkan, dan berkemas ke rumah untuk hari itu segera setelah melihatnya masuk.
Steve Jessop
3
+1 ekstra (jika saya bisa) untuk PS Anda
Fred Larson
10

Status standar pada 1.9 / 4

[Catatan: Standar Internasional ini tidak memberlakukan persyaratan pada perilaku program yang mengandung perilaku tidak terdefinisi. - catatan akhir]

Hal yang menarik mungkin adalah apa artinya "berisi". Beberapa saat kemudian pada 1.9 / 5 dinyatakan:

Namun, jika eksekusi semacam itu mengandung operasi yang tidak ditentukan, Standar Internasional ini tidak mensyaratkan pelaksanaan yang menjalankan program itu dengan masukan itu (bahkan tidak berkaitan dengan operasi sebelum operasi pertama yang tidak ditentukan)

Di sini secara khusus menyebutkan "eksekusi ... dengan masukan itu". Saya akan menafsirkannya sebagai, perilaku tidak terdefinisi dalam satu kemungkinan cabang yang tidak dieksekusi saat ini tidak mempengaruhi cabang eksekusi saat ini.

Namun masalah yang berbeda adalah asumsi yang didasarkan pada perilaku tidak terdefinisi selama pembuatan kode. Simak jawaban Steve Jessop untuk lebih jelasnya tentang itu.

Danvil
sumber
1
Jika diartikan secara harfiah, itu adalah hukuman mati untuk semua program nyata yang ada.
usr
6
Saya tidak berpikir pertanyaannya adalah apakah UB mungkin muncul sebelum kode benar-benar tercapai. Pertanyaannya, setahu saya, apakah UB bisa muncul kalau kodenya malah tidak tercapai. Dan tentu saja jawabannya adalah "tidak".
sepp2k
Sebenarnya standarnya tidak begitu jelas tentang ini di 1.9 / 4, tetapi 1.9 / 5 mungkin dapat diartikan sebagai apa yang Anda katakan.
Danvil
1
Catatan tidak normatif. 1.9 / 5 mengalahkan catatan di 1.9 / 4
MSalters
5

Contoh instruktif adalah

int foo(int x)
{
    int a;
    if (x)
        return a;
    return 0;
}

GCC saat ini dan Clang saat ini akan mengoptimalkan ini (pada x86) menjadi

xorl %eax,%eax
ret

karena mereka menyimpulkan bahwa xselalu nol dari UB di if (x)jalur kendali. GCC bahkan tidak akan memberi Anda peringatan use-of-uninitialized-value! (karena pass yang menerapkan logika di atas berjalan sebelum pass yang menghasilkan peringatan nilai yang tidak diinisialisasi)

zwol
sumber
1
Contoh yang menarik. Agak buruk bahwa mengaktifkan pengoptimalan menyembunyikan peringatan. Ini bahkan tidak didokumentasikan - dokumen GCC hanya mengatakan bahwa mengaktifkan pengoptimalan menghasilkan lebih banyak peringatan.
sleske
@sleske Ini menjijikkan, saya setuju, tetapi peringatan nilai yang tidak diinisialisasi sangat sulit untuk "dilakukan dengan benar" - melakukannya dengan sempurna sama dengan Masalah Menghentikan, dan pemrogram menjadi sangat tidak rasional tentang menambahkan inisialisasi variabel yang "tidak perlu" untuk memadamkan positif palsu, jadi penulis penyusun akhirnya berada di atas tong. Saya biasa meretas GCC dan saya ingat bahwa semua orang takut untuk mengacaukan kartu peringatan nilai yang tidak diinisialisasi.
zwol
@zwol: Saya ingin tahu seberapa banyak "pengoptimalan" yang dihasilkan dari penghapusan kode mati seperti itu sebenarnya membuat kode yang berguna menjadi lebih kecil, dan berapa banyak yang akhirnya menyebabkan pemrogram membuat kode lebih besar (misalnya dengan menambahkan kode untuk menginisialisasi abahkan jika dalam semua situasi di mana tidak diinisialisasi aakan diteruskan ke fungsi yang fungsi itu tidak akan pernah melakukan apa pun dengannya)?
supercat
@supercat Saya belum terlalu terlibat dalam pekerjaan kompilator selama ~ 10 tahun, dan hampir tidak mungkin untuk bernalar tentang pengoptimalan dari contoh mainan. Ini jenis optimasi cenderung dikaitkan dengan 2-5% pengurangan keseluruhan kode-size pada aplikasi nyata, jika saya ingat dengan benar.
zwol
1
@supercat 2-5% sangat besar karena hal-hal ini berjalan. Saya telah melihat orang-orang berkeringat 0,1%.
zwol
4

Draf kerja C ++ saat ini mengatakan di 1.9.4 bahwa

Standar Internasional ini tidak membebankan persyaratan pada perilaku program yang mengandung perilaku yang tidak ditentukan.

Berdasarkan ini, saya akan mengatakan bahwa program yang berisi perilaku tidak terdefinisi pada jalur eksekusi apa pun dapat melakukan apa saja setiap kali eksekusinya.

Ada dua artikel yang sangat bagus tentang perilaku tidak terdefinisi dan apa yang biasanya dilakukan oleh kompiler:

Jens
sumber
1
Itu tidak masuk akal. Fungsi tersebut int f(int x) { if (x > 0) return 100/x; else return 100; }tentu tidak pernah memunculkan perilaku yang tidak terdefinisi, meskipun 100/0tentu saja tidak terdefinisi.
fgp
1
@fgp Apa yang dikatakan oleh standar (terutama 1.9 / 5) adalah bahwa jika perilaku tidak terdefinisi dapat dicapai, tidak masalah kapan itu tercapai. Misalnya, printf("Hello, World"); *((char*)NULL) = 0 tidak ada jaminan untuk mencetak apa pun. Ini membantu pengoptimalan, karena kompilator dapat dengan bebas mengatur ulang operasi (tergantung pada batasan ketergantungan, tentu saja) yang diketahuinya akan terjadi pada akhirnya, tanpa harus memperhitungkan perilaku yang tidak ditentukan.
fgp
Saya akan mengatakan bahwa program dengan fungsi Anda tidak mengandung perilaku tidak terdefinisi, karena tidak ada masukan di mana 100/0 akan dievaluasi.
Jens
1
Persis - jadi yang penting apakah UB benar-benar bisa dipicu atau tidak, bukan apakah secara teori bisa dipicu. Ataukah Anda siap berargumen bahwa int x,y; std::cin >> x >> y; std::cout << (x+y);boleh mengatakan bahwa "1 + 1 = 17", hanya karena ada beberapa masukan yang x+ymelimpah (yang merupakan UB karena intmerupakan tipe yang ditandatangani).
fgp
Secara formal, saya akan mengatakan bahwa program tersebut memiliki perilaku yang tidak terdefinisi karena ada input yang memicunya. Tetapi Anda benar bahwa ini tidak masuk akal dalam konteks C ++, karena tidak mungkin untuk menulis program tanpa perilaku yang tidak ditentukan. Saya ingin jika ada perilaku yang kurang terdefinisi di C ++, tetapi bukan itu cara kerja bahasanya (dan ada beberapa alasan bagus untuk ini, tetapi itu tidak menyangkut penggunaan sehari-hari saya ...).
Jens
3

Kata "perilaku" berarti sesuatu sedang dilakukan . Sebuah statemenr yang tidak pernah dieksekusi bukanlah "perilaku".

Sebuah ilustrasi:

*ptr = 0;

Apakah itu perilaku yang tidak terdefinisi? Misalkan kita 100% yakin ptr == nullptrsetidaknya sekali selama eksekusi program. Jawabannya harus ya.

Bagaimana dengan ini?

 if (ptr) *ptr = 0;

Apakah itu tidak ditentukan? (Ingat ptr == nullptrsetidaknya sekali?) Saya harap tidak, jika tidak, Anda tidak akan bisa menulis program yang berguna sama sekali.

Tidak ada orang srandard yang dirugikan dalam membuat jawaban ini.

n. 'kata ganti' m.
sumber
3

Perilaku tidak terdefinisi menyerang ketika program akan menyebabkan perilaku tak terdefinisi apa pun yang terjadi selanjutnya. Namun, Anda memberi contoh berikut.

int num = ReadNumberFromConsole();

if (num == 3) {
 PrintToConsole(num);
 *((char*)NULL) = 0; //undefined behavior
}

Kecuali jika kompilator mengetahui definisinya PrintToConsole, ia tidak dapat menghapus if (num == 3)kondisional. Anggaplah Anda memiliki LongAndCamelCaseStdio.hheader sistem dengan deklarasi berikut ini PrintToConsole.

void PrintToConsole(int);

Tidak terlalu membantu, oke. Sekarang, mari kita lihat seberapa jahat (atau mungkin tidak begitu jahat, perilaku tidak terdefinisi bisa lebih buruk) vendor, dengan memeriksa definisi sebenarnya dari fungsi ini.

int printf(const char *, ...);
void exit(int);

void PrintToConsole(int num) {
    printf("%d\n", num);
    exit(0);
}

Kompilator sebenarnya harus berasumsi bahwa sembarang fungsi yang tidak diketahui oleh kompilator dapat keluar atau memunculkan pengecualian (dalam kasus C ++). Anda dapat melihat bahwa *((char*)NULL) = 0;tidak akan dieksekusi, karena eksekusi tidak akan dilanjutkan setelah PrintToConsolepanggilan.

Perilaku tidak terdefinisi menyerang kapan PrintToConsole benar-benar kembali. Kompilator mengharapkan ini tidak terjadi (karena ini akan menyebabkan program mengeksekusi perilaku tidak terdefinisi apapun yang terjadi), oleh karena itu apapun bisa terjadi.

Namun, mari pertimbangkan hal lain. Katakanlah kita melakukan pemeriksaan null, dan menggunakan variabel setelah pemeriksaan null.

int putchar(int);

const char *warning;

void lol_null_check(const char *pointer) {
    if (!pointer) {
        warning = "pointer is null";
    }
    putchar(*pointer);
}

Dalam hal ini, mudah untuk diperhatikan bahwa lol_null_checkmembutuhkan pointer non-NULL. Menetapkan ke warningvariabel non-volatile global bukanlah sesuatu yang dapat keluar dari program atau memunculkan pengecualian apa pun. Ini pointerjuga non-volatile, sehingga tidak dapat secara ajaib mengubah nilainya di tengah fungsi (jika ya, itu adalah perilaku yang tidak ditentukan). Panggilanlol_null_check(NULL) akan menyebabkan perilaku tidak terdefinisi yang dapat menyebabkan variabel tidak ditetapkan (karena pada titik ini, fakta bahwa program mengeksekusi perilaku tidak terdefinisi diketahui).

Namun, perilaku tidak terdefinisi berarti program dapat melakukan apa saja. Oleh karena itu, tidak ada yang menghentikan perilaku tidak terdefinisi dari kembali ke masa lalu, dan menghentikan program Anda sebelum baris pertama int main()eksekusi. Ini adalah perilaku yang tidak jelas, tidak harus masuk akal. Mungkin juga macet setelah mengetik 3, tetapi perilaku yang tidak ditentukan akan kembali ke masa lalu, dan macet bahkan sebelum Anda mengetik 3. Dan siapa tahu, mungkin perilaku yang tidak ditentukan akan menimpa RAM sistem Anda, dan menyebabkan sistem Anda macet 2 minggu kemudian, sementara program Anda yang tidak ditentukan tidak berjalan.

Konrad Borowski
sumber
Semua poin yang valid. PrintToConsoleadalah usaha saya untuk memasukkan efek samping program-eksternal yang terlihat bahkan setelah crash dan diurutkan dengan kuat. Saya ingin membuat situasi di mana kami dapat memastikan dengan pasti apakah pernyataan ini dioptimalkan. Tetapi Anda benar bahwa hal itu mungkin tidak akan pernah kembali .; Contoh Anda menulis ke global mungkin tunduk pada pengoptimalan lain yang tidak terkait dengan UB. Misalnya global yang tidak digunakan dapat dihapus. Apakah Anda memiliki ide untuk membuat efek samping eksternal dengan cara yang dijamin dapat mengembalikan kendali?
usr
Dapatkah efek samping yang dapat diamati dunia luar dihasilkan oleh kode yang kompiler bebas untuk mengasumsikan pengembalian? Menurut pemahaman saya, bahkan metode yang hanya membaca volatilevariabel dapat secara sah memicu operasi I / O yang pada gilirannya dapat segera mengganggu thread saat ini; penangan interupsi kemudian dapat mematikan utas sebelum memiliki kesempatan untuk melakukan hal lain. Saya tidak melihat pembenaran yang digunakan kompilator untuk mendorong perilaku tidak terdefinisi sebelum titik itu.
supercat
Dari sudut pandang standar C, tidak ada yang ilegal tentang Perilaku Tidak Terdefinisi yang menyebabkan komputer mengirim pesan ke beberapa orang yang akan melacak dan menghancurkan semua bukti tindakan sebelumnya dari program, tetapi jika suatu tindakan dapat menghentikan utas, maka segala sesuatu yang diurutkan sebelum tindakan itu harus terjadi sebelum Perilaku Tidak Terdefinisi apa pun yang terjadi setelahnya.
supercat
1

Jika program mencapai pernyataan yang memanggil perilaku tidak terdefinisi, tidak ada persyaratan yang ditempatkan pada keluaran / perilaku program apa pun; tidak peduli apakah mereka akan terjadi "sebelum" atau "setelah" perilaku tidak terdefinisi dipanggil.

Alasan Anda tentang ketiga cuplikan kode itu benar. Secara khusus, kompilator dapat memperlakukan pernyataan apa pun yang tanpa syarat memanggil perilaku tidak terdefinisi seperti yang diperlakukan oleh GCC __builtin_unreachable(): sebagai petunjuk pengoptimalan bahwa pernyataan itu tidak dapat dijangkau (dan karenanya, semua jalur kode yang mengarah tanpa syarat ke sana juga tidak dapat dijangkau). Pengoptimalan serupa lainnya tentu saja dimungkinkan.

R .. GitHub BERHENTI ICE BANTUAN
sumber
1
Karena penasaran, kapan __builtin_unreachable()mulai memiliki efek yang berjalan mundur dan maju dalam waktu? Diberikan sesuatu seperti extern volatile uint32_t RESET_TRIGGER; void RESET(void) { RESET_TRIGGER = 0xAA55; __memorybarrier(); __builtin_unreachable(); }saya bisa melihat hal builtin_unreachable()yang baik untuk memberi tahu kompiler itu dapat menghilangkan returninstruksi, tetapi itu akan agak berbeda dari mengatakan bahwa kode sebelumnya dapat dihilangkan.
supercat
@supercat karena RESET_TRIGGER tidak stabil, penulisan ke lokasi tersebut dapat memiliki efek samping yang berubah-ubah. Bagi kompiler, ini seperti panggilan metode buram. Oleh karena itu, tidak dapat dibuktikan (dan bukan kasusnya) yang __builtin_unreachabletercapai. Program ini ditentukan.
usr
@usr: Saya akan berpikir bahwa kompiler tingkat rendah harus memperlakukan akses volatil sebagai panggilan metode buram, tetapi baik clang maupun gcc tidak melakukannya. Antara lain, panggilan metode buram dapat menyebabkan semua byte ke objek apa pun yang alamatnya telah diekspos ke dunia luar, dan yang tidak pernah atau tidak akan diakses oleh restrictpenunjuk langsung , untuk ditulis menggunakan unsigned char*.
supercat
@usr: Jika kompilator tidak memperlakukan akses yang mudah menguap sebagai panggilan metode buram sehubungan dengan akses ke objek yang diekspos, saya tidak melihat alasan khusus untuk mengharapkan bahwa itu akan melakukannya untuk tujuan lain. Standar tidak mengharuskan implementasi melakukannya, karena ada beberapa platform perangkat keras di mana kompilator mungkin dapat mengetahui semua kemungkinan efek dari akses volatil. Sebuah kompilator yang cocok untuk penggunaan yang disematkan, bagaimanapun, harus mengenali bahwa akses volatil dapat memicu perangkat keras yang belum ditemukan ketika kompilator ditulis.
supercat
@upercy saya pikir Anda benar. Tampaknya operasi yang mudah menguap "tidak berpengaruh pada mesin abstrak" dan oleh karena itu tidak dapat menghentikan program atau menyebabkan efek samping.
usr
1

Banyak standar untuk berbagai macam hal menghabiskan banyak usaha untuk menjelaskan hal-hal yang HARUS atau HARUS dilakukan oleh implementasi, menggunakan nomenklatur yang mirip dengan yang didefinisikan dalam IETF RFC 2119 (meskipun tidak harus mengutip definisi dalam dokumen itu). Dalam banyak kasus, deskripsi tentang hal-hal yang harus dilakukan implementasi kecuali dalam kasus di mana hal itu tidak berguna atau tidak praktis lebih penting daripada persyaratan yang harus dipatuhi oleh semua implementasi yang sesuai.

Sayangnya, Standar C dan C ++ cenderung menghindari deskripsi hal-hal yang, meskipun tidak 100% diperlukan, namun harus diharapkan dari implementasi berkualitas yang tidak mendokumentasikan perilaku yang berlawanan. Sebuah saran bahwa implementasi harus melakukan sesuatu dapat dilihat sebagai menyiratkan bahwa mereka yang tidak inferior, dan dalam kasus di mana umumnya akan jelas perilaku mana yang akan berguna atau praktis, versus tidak praktis dan tidak berguna, pada implementasi tertentu, ada sedikit kebutuhan yang dirasakan Standar untuk mengganggu penilaian tersebut.

Kompilator yang pintar dapat menyesuaikan dengan Standar sambil menghilangkan kode apa pun yang tidak akan berpengaruh kecuali ketika kode menerima input yang pasti akan menyebabkan Perilaku Tidak Terdefinisi, tetapi "pintar" dan "bodoh" bukanlah antonim. Fakta bahwa penulis Standar memutuskan bahwa mungkin ada beberapa jenis implementasi di mana berperilaku berguna dalam situasi tertentu tidak berguna dan tidak praktis tidak menyiratkan penilaian apa pun, apakah perilaku tersebut harus dianggap praktis dan berguna bagi orang lain. Jika suatu implementasi dapat mempertahankan jaminan perilaku tanpa biaya selain hilangnya peluang pemangkasan "cabang mati", hampir semua nilai yang dapat diterima kode pengguna dari jaminan tersebut akan melebihi biaya penyediaannya. Penghapusan cabang mati mungkin baik-baik saja dalam kasus di mana tidak perlu menyerah, Tetapi jika dalam situasi kode pengguna yang diberikan bisa ditangani hampir semua perilaku yang mungkin lain dari eliminasi mati-cabang, setiap upaya kode pengguna harus mengeluarkan menghindari UB kemungkinan akan melebihi nilai yang dicapai dari DBE.

supercat
sumber
Ada baiknya menghindari UB dapat membebankan biaya pada kode pengguna.
usr
@usr: Ini adalah poin yang benar-benar dilewatkan kaum modernis. Haruskah saya menambahkan contoh? Misalnya, jika kode perlu mengevaluasi x*y < zkapan x*ytidak meluap, dan dalam kasus luapan menghasilkan 0 atau 1 secara sewenang-wenang tetapi tanpa efek samping, tidak ada alasan pada sebagian besar platform mengapa memenuhi persyaratan kedua dan ketiga harus lebih mahal daripada memenuhi yang pertama, tetapi cara penulisan ekspresi apa pun untuk menjamin perilaku yang ditentukan standar dalam semua kasus akan dalam beberapa kasus menambah biaya yang signifikan. Menulis ekspresi yang (int64_t)x*y < zdapat melipatgandakan biaya komputasi ...
supercat
... pada beberapa platform, dan menuliskannya seperti itu (int)((unsigned)x*y) < zakan mencegah kompilator menggunakan apa yang mungkin berguna sebagai substitusi aljabar (misalnya jika ia tahu itu xdan zsama dan positif, itu dapat menyederhanakan ekspresi asli menjadi y<0, tetapi versi menggunakan unsigned akan memaksa kompiler untuk melakukan perkalian). Jika kompilator dapat menjamin meskipun Standar tidak mengamanatkannya, ia akan mempertahankan persyaratan "hasil 0 atau 1 tanpa efek samping", kode pengguna dapat memberikan peluang pengoptimalan kompilator yang tidak dapat diperolehnya.
supercat
Ya, tampaknya beberapa bentuk perilaku tidak terdefinisi yang lebih ringan akan membantu di sini. Pemrogram dapat mengaktifkan mode yang menyebabkan x*ymemancarkan nilai normal jika terjadi luapan tetapi nilai apa pun sama sekali. UB yang dapat dikonfigurasi dalam C / C ++ tampaknya penting bagi saya.
usr
@usr: Jika penulis C89 Standard jujur ​​mengatakan bahwa promosi nilai unsigned pendek untuk masuk adalah perubahan yang paling serius, dan bukan orang bodoh yang bodoh, itu menyiratkan bahwa mereka mengharapkan itu dalam kasus di mana platform memiliki telah mendefinisikan jaminan perilaku yang berguna, implementasi untuk platform semacam itu telah membuat jaminan tersebut tersedia bagi pemrogram, dan pemrogram telah mengeksploitasinya, kompiler untuk platform semacam itu akan terus menawarkan jaminan perilaku seperti itu apakah Standar memerintahkannya atau tidak .
supercat