Saya tahu bahwa "perilaku tidak terdefinisi" di C ++ dapat memungkinkan kompilator melakukan apa pun yang diinginkannya. Namun, saya mengalami crash yang mengejutkan saya, karena saya berasumsi bahwa kode itu cukup aman.
Dalam kasus ini, masalah sebenarnya hanya terjadi pada platform tertentu menggunakan kompiler tertentu, dan hanya jika optimasi diaktifkan.
Saya mencoba beberapa hal untuk mereproduksi masalah dan menyederhanakannya secara maksimal. Berikut adalah ekstrak fungsi yang disebut Serialize
, yang akan mengambil parameter bool, dan menyalin string true
atau false
ke buffer tujuan yang ada.
Apakah fungsi ini dalam ulasan kode, tidak akan ada cara untuk mengatakan bahwa itu, pada kenyataannya, bisa macet jika parameter bool adalah nilai yang tidak diinisialisasi?
// Zero-filled global buffer of 16 characters
char destBuffer[16];
void Serialize(bool boolValue) {
// Determine which string to print based on boolValue
const char* whichString = boolValue ? "true" : "false";
// Compute the length of the string we selected
const size_t len = strlen(whichString);
// Copy string into destination buffer, which is zero-filled (thus already null-terminated)
memcpy(destBuffer, whichString, len);
}
Jika kode ini dijalankan dengan optimasi 5.0.0 clang 5.0, maka akan / bisa macet.
Operator ternary yang diharapkan boolValue ? "true" : "false"
tampak cukup aman bagi saya, saya berasumsi, "Apa pun nilai sampah boolValue
tidak masalah, karena bagaimanapun akan menilai benar atau salah."
Saya telah menyiapkan contoh Compiler Explorer yang menunjukkan masalah dalam pembongkaran, di sini contoh lengkapnya. Catatan: untuk repro masalah ini, kombinasi yang saya temukan yang berhasil adalah dengan menggunakan Dentang 5.0.0 dengan optimasi -O2.
#include <iostream>
#include <cstring>
// Simple struct, with an empty constructor that doesn't initialize anything
struct FStruct {
bool uninitializedBool;
__attribute__ ((noinline)) // Note: the constructor must be declared noinline to trigger the problem
FStruct() {};
};
char destBuffer[16];
// Small utility function that allocates and returns a string "true" or "false" depending on the value of the parameter
void Serialize(bool boolValue) {
// Determine which string to print depending if 'boolValue' is evaluated as true or false
const char* whichString = boolValue ? "true" : "false";
// Compute the length of the string we selected
size_t len = strlen(whichString);
memcpy(destBuffer, whichString, len);
}
int main()
{
// Locally construct an instance of our struct here on the stack. The bool member uninitializedBool is uninitialized.
FStruct structInstance;
// Output "true" or "false" to stdout
Serialize(structInstance.uninitializedBool);
return 0;
}
Masalah muncul karena pengoptimal: Itu cukup pintar untuk menyimpulkan bahwa string "benar" dan "salah" hanya berbeda panjangnya dengan 1. Jadi, alih-alih benar-benar menghitung panjangnya, ia menggunakan nilai bool itu sendiri, yang seharusnya secara teknis menjadi 0 atau 1, dan berjalan seperti ini:
const size_t len = strlen(whichString); // original code
const size_t len = 5 - boolValue; // clang clever optimization
Walaupun ini "pintar", jadi untuk pertanyaan, pertanyaan saya adalah: Apakah standar C ++ memungkinkan kompiler untuk menganggap bool hanya dapat memiliki representasi numerik internal '0' atau '1' dan menggunakannya sedemikian rupa?
Atau apakah ini kasus implementasi yang didefinisikan, dalam hal mana implementasi tersebut mengasumsikan bahwa semua bools hanya akan mengandung 0 atau 1, dan nilai lainnya adalah wilayah perilaku yang tidak terdefinisi?
true
" adalah aturan tentang operasi Boolean termasuk "penugasan ke bool" (yang mungkin secara implisit meminta astatic_cast<bool>()
tergantung pada spesifikasi). Namun itu bukan persyaratan tentang representasi internal yangbool
dipilih oleh kompiler.Jawaban:
Ya, ISO C ++ memungkinkan (tetapi tidak mengharuskan) implementasi untuk membuat pilihan ini.
Tetapi juga perhatikan bahwa ISO C ++ memungkinkan kompiler untuk memancarkan kode yang sengaja macet (misalnya dengan instruksi ilegal) jika program menemui UB, misalnya sebagai cara untuk membantu Anda menemukan kesalahan. (Atau karena itu adalah DeathStation 9000. Menjadi benar-benar menyesuaikan saja tidak cukup untuk implementasi C ++ berguna untuk tujuan nyata apa pun). Jadi ISO C ++ akan memungkinkan kompiler untuk membuat asm yang crash (untuk alasan yang sama sekali berbeda) bahkan pada kode serupa yang membaca yang tidak diinisialisasi
uint32_t
. Meskipun itu diperlukan tipe tata letak tetap tanpa representasi trap.Ini adalah pertanyaan menarik tentang bagaimana implementasi nyata bekerja, tetapi ingat bahwa meskipun jawabannya berbeda, kode Anda tetap tidak aman karena C ++ modern bukan versi portabel bahasa rakitan.
Anda sedang mengkompilasi untuk System V ABI x86-64 , yang menentukan bahwa
bool
sebagai fungsi arg dalam register diwakili oleh pola-bitfalse=0
dantrue=1
dalam bit 8 yang rendah dari register 1 . Dalam memori,bool
adalah tipe 1-byte yang lagi-lagi harus memiliki nilai integer 0 atau 1.(ABI adalah sekumpulan pilihan implementasi yang disetujui oleh penyusun untuk platform yang sama sehingga mereka dapat membuat kode yang memanggil fungsi masing-masing, termasuk ukuran tipe, aturan tata letak struct, dan konvensi pemanggilan.)
ISO C ++ tidak menentukannya, tetapi keputusan ABI ini tersebar luas karena membuat konversi bool-> int menjadi murah (hanya ekstensi-nol) . Saya tidak mengetahui adanya ABI yang tidak membiarkan kompiler menganggap 0 atau 1 untuk
bool
, untuk arsitektur apa pun (bukan hanya x86). Ini memungkinkan optimasi seperti!mybool
denganxor eax,1
membalik bit rendah: Setiap kode yang mungkin dapat membalik sedikit / integer / bool antara 0 dan 1 dalam instruksi CPU tunggal . Atau mengkompilasia&&b
ke bitwise DAN untukbool
jenis. Beberapa kompiler benar-benar memanfaatkan nilai Boolean sebagai 8 bit dalam kompiler. Apakah operasi pada mereka tidak efisien? .Secara umum, aturan as-if memungkinkan memungkinkan kompiler untuk mengambil keuntungan dari hal-hal yang benar pada platform target yang dikompilasi , karena hasil akhirnya akan menjadi kode yang dapat dieksekusi yang mengimplementasikan perilaku yang terlihat secara eksternal sama seperti sumber C ++. (Dengan semua pembatasan yang dilakukan Perilaku Tidak Terdefinisi pada apa yang sebenarnya "terlihat secara eksternal": tidak dengan debugger, tetapi dari utas lain dalam program C ++ yang legal / baik.)
Compiler pasti diizinkan untuk mengambil keuntungan penuh dari jaminan ABI di nya kode-gen, dan membuat kode seperti Anda menemukan yang mengoptimalkan
strlen(whichString)
untuk5U - boolValue
. (BTW, optimasi ini agak pintar, tapi mungkin picik vs bercabang dan inliningmemcpy
sebagai penyimpan data langsung 2. )Atau kompiler bisa saja membuat tabel pointer dan mengindeksnya dengan nilai integer
bool
, sekali lagi dengan anggapan itu adalah 0 atau 1. ( Kemungkinan inilah yang disarankan oleh jawaban Barmar .)__attribute((noinline))
Konstruktor Anda dengan optimisasi diaktifkan menyebabkan hanya memuat byte dari tumpukan untuk digunakan sebagaiuninitializedBool
. Itu membuat ruang untuk objekmain
denganpush rax
(yang lebih kecil dan karena berbagai alasan tentang seefisiensub rsp, 8
), jadi apa pun sampah di AL pada entrimain
adalah nilai yang digunakan untuk ituuninitializedBool
. Inilah sebabnya mengapa Anda benar-benar mendapatkan nilai yang tidak adil0
.5U - random garbage
dapat dengan mudah membungkus ke nilai yang tidak ditandatangani besar, memimpin memcpy untuk masuk ke memori yang belum dipetakan. Tujuannya adalah penyimpanan statis, bukan tumpukan, jadi Anda tidak menimpa alamat pengirim atau sesuatu.Implementasi lain dapat membuat pilihan yang berbeda, misalnya
false=0
dantrue=any non-zero value
. Kemudian dentang mungkin tidak akan membuat kode yang crash untuk ini contoh spesifik dari UB. (Tapi itu masih akan diizinkan jika ingin.) Saya tidak tahu implementasi apa pun yang memilih untuk apa x86-64 dilakukanbool
, tetapi standar C ++ memungkinkan banyak hal yang tidak dilakukan oleh siapa pun atau bahkan ingin dilakukan pada perangkat keras yang mirip dengan CPU saat ini.ISO C ++ membiarkannya tidak ditentukan apa yang akan Anda temukan ketika Anda memeriksa atau memodifikasi representasi objek dari a
bool
. (misalnya denganmemcpy
memasukkanbool
ke dalamunsigned char
, yang diizinkan untuk Anda lakukan karenachar*
bisa alias apa saja. Danunsigned char
dijamin tidak memiliki bit padding, sehingga standar C ++ memungkinkan Anda secara hexdump representasi objek tanpa UB. Pointer-casting untuk menyalin objek representasi berbeda dari penetapanchar foo = my_bool
, tentu saja, jadi booleanisasi ke 0 atau 1 tidak akan terjadi dan Anda akan mendapatkan representasi objek mentah.)Anda telah sebagian "menyembunyikan" UB di jalur eksekusi ini dari kompiler dengan
noinline
. Meskipun tidak sejajar, optimasi interprocedural masih bisa membuat versi fungsi yang tergantung pada definisi fungsi lain. (Pertama, dentang membuat yang dapat dieksekusi, bukan perpustakaan bersama Unix di mana simbol-interposisi dapat terjadi. Kedua, definisi di dalamclass{}
definisi sehingga semua unit terjemahan harus memiliki definisi yang sama. Seperti denganinline
kata kunci.)Jadi penyusun dapat memancarkan hanya
ret
atauud2
(instruksi ilegal) sebagai definisi untukmain
, karena jalur eksekusi dimulai dari atas yangmain
tak terhindarkan menghadapi Perilaku Tidak Terdefinisi. (Yang dapat dilihat kompilator pada waktu kompilasi jika memutuskan untuk mengikuti jalur melalui konstruktor non-inline.)Program apa pun yang bertemu UB benar-benar tidak ditentukan untuk seluruh keberadaannya. Tetapi UB di dalam fungsi atau
if()
cabang yang tidak pernah benar-benar berjalan tidak merusak sisa program. Dalam prakteknya itu berarti bahwa penyusun dapat memutuskan untuk mengeluarkan instruksi ilegal, atauret
, atau tidak memancarkan apa pun dan jatuh ke blok / fungsi berikutnya, untuk seluruh blok dasar yang dapat dibuktikan pada waktu kompilasi untuk mengandung atau mengarah ke UB.GCC dan Dentang dalam praktek kadang-kadang benar - benar memancarkan
ud2
di UB, bukannya mencoba menghasilkan kode untuk jalur eksekusi yang tidak masuk akal. Atau untuk kasus-kasus seperti jatuh dari ujung non-void
fungsi, gcc terkadang akan menghilangkanret
instruksi. Jika Anda berpikir bahwa "fungsi saya hanya akan kembali dengan sampah apa pun di RAX", Anda salah besar. Kompiler C ++ modern tidak lagi memperlakukan bahasa seperti bahasa rakitan portabel. Program Anda benar-benar harus valid C ++, tanpa membuat asumsi tentang bagaimana versi yang berdiri sendiri dari fungsi Anda mungkin terlihat dalam asm.Contoh lain yang menyenangkan adalah Mengapa akses yang tidak selaras ke memori mmap'ed kadang-kadang terpisah pada AMD64? . x86 tidak kesalahan pada bilangan bulat yang tidak selaras, kan? Jadi mengapa orang yang tidak selaras
uint16_t*
menjadi masalah? Karenaalignof(uint16_t) == 2
, dan melanggar asumsi itu menyebabkan segfault ketika auto-vectorizing dengan SSE2.Lihat juga Apa yang Harus Diketahui Setiap Pemrogram C Tentang Perilaku Tidak Terdefinisi # 1/3 , sebuah artikel oleh pengembang dentang.
Poin kunci: jika kompiler memperhatikan UB pada waktu kompilasi, itu bisa "memecah" (memancarkan asm yang mengejutkan) jalur melalui kode Anda yang menyebabkan UB bahkan jika menargetkan ABI di mana setiap bit-pola adalah representasi objek yang valid untuk
bool
.Harapkan permusuhan total terhadap banyak kesalahan oleh programmer, terutama hal-hal yang diingatkan oleh kompiler modern. Inilah sebabnya mengapa Anda harus menggunakan
-Wall
dan memperbaiki peringatan. C ++ bukan bahasa yang ramah pengguna, dan sesuatu dalam C ++ bisa tidak aman bahkan jika itu akan aman dalam asm pada target yang Anda kompilasi. (mis. Overflow yang ditandatangani adalah UB dalam C ++ dan kompiler akan menganggap itu tidak terjadi, bahkan ketika mengkompilasi untuk komplemen 2 x86, kecuali jika Anda menggunakannyaclang/gcc -fwrapv
.)Kompilasi-waktu-kelihatan UB selalu berbahaya, dan sangat sulit untuk memastikan (dengan optimasi tautan-waktu) bahwa Anda telah benar-benar menyembunyikan UB dari kompiler dan karenanya dapat alasan tentang jenis asm yang akan dihasilkannya.
Tidak terlalu dramatis; sering kompiler membiarkan Anda lolos dengan beberapa hal dan memancarkan kode seperti yang Anda harapkan bahkan ketika ada sesuatu yang UB. Tapi mungkin itu akan menjadi masalah di masa depan jika compiler dev menerapkan beberapa optimasi yang memperoleh lebih banyak info tentang rentang nilai (misalnya bahwa variabel tidak negatif, mungkin memungkinkannya untuk mengoptimalkan ekstensi-tanda untuk membebaskan nol-ekstensi pada x86- 64). Misalnya, dalam gcc dan dentang saat ini, melakukan
tmp = a+INT_MIN
tidak mengoptimalkana<0
sebagai selalu-salah, hanya sajatmp
selalu negatif. (KarenaINT_MIN
+a=INT_MAX
negatif pada target komplemen 2 ini, dana
tidak mungkin lebih tinggi dari itu.)Jadi gcc / dentang saat ini tidak mundur untuk mendapatkan info rentang untuk input perhitungan, hanya pada hasil berdasarkan asumsi tidak ada limpahan ditandatangani: contoh pada Godbolt . Saya tidak tahu apakah ini optimasi yang sengaja "dilewatkan" atas nama keramahan pengguna atau apa.
Perhatikan juga bahwa implementasi (kompiler alias) diizinkan untuk mendefinisikan perilaku yang tidak ditentukan oleh ISO C ++ . Sebagai contoh, semua kompiler yang mendukung intrinsik Intel (seperti
_mm_add_ps(__m128, __m128)
untuk vektorisasi SIMD manual) harus memungkinkan pembentukan pointer yang tidak sejajar, yaitu UB dalam C ++ bahkan jika Anda tidak melakukan dereferensi.__m128i _mm_loadu_si128(const __m128i *)
melakukan banyak unaligned dengan mengambil__m128i*
argumen yang tidak selaras , bukan avoid*
atauchar*
. Apakah `reinterpret_cast`ing antara pointer vektor perangkat keras dan tipe yang sesuai merupakan perilaku yang tidak terdefinisi?GNU C / C ++ juga mendefinisikan perilaku menggeser angka bertanda negatif (bahkan tanpa
-fwrapv
), secara terpisah dari aturan UB yang ditandatangani-limpahan normal. ( Ini adalah UB dalam ISO C ++ , sementara pergeseran kanan dari angka yang ditandatangani didefinisikan implementasi (logis vs aritmatika); implementasi berkualitas baik memilih aritmatika pada HW yang memiliki pergeseran aritmatika yang benar, tetapi ISO C ++ tidak menentukan). Ini didokumentasikan di bagian Integer manual GCC , bersama dengan mendefinisikan perilaku yang didefinisikan implementasi yang standar C membutuhkan implementasi untuk menentukan satu atau lain cara.Pasti ada masalah kualitas implementasi yang diperhatikan pengembang kompiler; mereka umumnya tidak mencoba membuat kompiler yang sengaja dimusuhi, tetapi mengambil keuntungan dari semua lubang UB di C ++ (kecuali yang mereka pilih untuk didefinisikan) untuk mengoptimalkan yang lebih baik kadang-kadang hampir tidak bisa dibedakan.
Catatan Kaki 1 : 56 bit bagian atas dapat berupa sampah yang harus diabaikan oleh callee, seperti biasa untuk tipe yang lebih sempit daripada register.
( ABI lain memang membuat pilihan berbeda di sini . Beberapa memang membutuhkan tipe integer sempit menjadi nol atau diperpanjang untuk mengisi register ketika diteruskan ke atau dikembalikan dari fungsi, seperti MIPS64 dan PowerPC64. Lihat bagian terakhir dari jawaban x86-64 ini yang membandingkan vs. ISA sebelumnya .)
Misalnya, seorang penelepon mungkin telah menghitung
a & 0x01010101
dalam RDI dan menggunakannya untuk hal lain, sebelum meneleponbool_func(a&1)
. Penelepon dapat mengoptimalkan jauh&1
karena sudah melakukan itu ke byte rendah sebagai bagian dariand edi, 0x01010101
, dan ia tahu callee diperlukan untuk mengabaikan byte tinggi.Atau jika bool dilewatkan sebagai argumen ke-3, mungkin penelepon yang mengoptimalkan ukuran kode memuatnya,
mov dl, [mem]
bukannyamovzx edx, [mem]
menghemat 1 byte dengan biaya ketergantungan salah pada nilai RDX yang lama (atau efek register parsial lainnya, tergantung pada model CPU). Atau untuk argumen pertama,mov dil, byte [r10]
alih-alihmovzx edi, byte [r10]
, karena keduanya memerlukan awalan REX.Inilah sebabnya mengapa memancarkan dentang
movzx eax, dil
diSerialize
, bukansub eax, edi
. (Untuk argumen integer, dentang melanggar aturan ABI ini, sebagai gantinya tergantung pada perilaku tidak berdokumen dari gcc dan dentang ke nol atau perpanjangan tanda integer sempit menjadi 32 bit. Merupakan tanda atau ekstensi nol yang diperlukan saat menambahkan offset 32bit ke pointer untuk ABI x86-64? Jadi saya tertarik untuk melihat bahwa itu tidak melakukan hal yang sama untukbool
.)Catatan kaki 2: Setelah bercabang, Anda hanya akan memiliki 4-byte
mov
-dimateate, atau 4-byte + 1-byte store. Panjangnya tersirat dalam lebar toko + offset.OTOH, memcpy glibc akan melakukan dua beban / toko 4-byte dengan tumpang tindih yang tergantung pada panjang, jadi ini benar-benar membuat semuanya bebas dari cabang-cabang kondisional di boolean. Lihat
L(between_4_7):
blok di memcpy / memmove glibc. Atau setidaknya, lakukan cara yang sama untuk boolean di percabangan memcpy untuk memilih ukuran chunk.Jika inlining, Anda bisa menggunakan 2x
mov
-immediate +cmov
dan offset bersyarat, atau Anda bisa meninggalkan data string dalam memori.Atau jika mencari Intel Ice Lake ( dengan fitur Fast Short REP MOV ), yang sebenarnya
rep movsb
mungkin optimal. glibcmemcpy
mungkin mulai digunakanrep movsb
untuk ukuran kecil pada CPU dengan fitur itu, menghemat banyak percabangan.Alat untuk mendeteksi UB dan penggunaan nilai yang tidak diinisialisasi
Di gcc dan dentang, Anda dapat mengkompilasi dengan
-fsanitize=undefined
menambahkan run-time instrumentation yang akan memperingatkan atau kesalahan pada UB yang terjadi saat runtime. Itu tidak akan menangkap variabel unitial. (Karena itu tidak menambah ukuran tipe untuk memberi ruang bagi bit "tidak diinisialisasi").Lihat https://developers.redhat.com/blog/2014/10/16/gcc-undefined-behavior-sanitizer-ubsan/
Untuk menemukan penggunaan data yang tidak diinisialisasi, ada Sanitizer Alamat dan Memory Sanitizer di dentang / LLVM. https://github.com/google/sanitizers/wiki/MemorySanitizer menunjukkan contoh-contoh
clang -fsanitize=memory -fPIE -pie
pendeteksian memori yang tidak diinisialisasi. Ini mungkin bekerja paling baik jika Anda mengkompilasi tanpa optimasi, jadi semua membaca variabel akhirnya memuat dari memori dalam asm. Mereka menunjukkan itu digunakan di-O2
dalam kasus di mana beban tidak akan optimal. Saya belum mencobanya sendiri. (Dalam beberapa kasus, misalnya tidak menginisialisasi akumulator sebelum menjumlahkan array, dentang -O3 akan memancarkan kode yang menjumlahkan ke register vektor yang tidak pernah diinisialisasi. Jadi dengan optimisasi, Anda dapat memiliki kasus di mana tidak ada memori yang dibaca terkait dengan UB Tapi-fsanitize=memory
mengubah asm yang dihasilkan, dan mungkin menghasilkan cek untuk ini.)Seharusnya berfungsi untuk kasus ini karena panggilan ke glibc
memcpy
denganlength
dihitung dari memori yang tidak diinisialisasi akan (di dalam perpustakaan) menghasilkan cabang berdasarkanlength
. Jika itu meringkas versi tanpa cabang yang hanya digunakancmov
, mengindeks, dan dua toko, itu mungkin tidak akan berfungsi.Valgrind's
memcheck
juga akan mencari masalah seperti ini, sekali lagi tidak mengeluh jika program hanya menyalin sekitar data yang tidak diinisialisasi. Tetapi ia mengatakan akan mendeteksi kapan "lompatan kondisional atau bergerak tergantung pada nilai yang tidak diinisialisasi", untuk mencoba menangkap perilaku yang terlihat secara eksternal yang tergantung pada data yang tidak diinisialisasi.Mungkin ide di balik tidak menandai hanya sebuah beban adalah bahwa struct dapat memiliki padding, dan menyalin seluruh struct (termasuk padding) dengan beban vektor yang luas / toko bukan kesalahan bahkan jika anggota individu hanya ditulis satu per satu. Pada tingkat asm, informasi tentang apa yang padding dan apa yang sebenarnya merupakan bagian dari nilai telah hilang.
sumber
Compiler diperbolehkan untuk berasumsi bahwa nilai boolean yang diteruskan sebagai argumen adalah nilai boolean yang valid (yaitu yang telah diinisialisasi atau dikonversi ke
true
ataufalse
). Thetrue
nilai tidak harus sama dengan bilangan bulat 1 - memang, bisa ada berbagai representasi daritrue
danfalse
- tetapi parameter harus beberapa representasi valid dari salah satu dari dua nilai, di mana "representasi yang sah" adalah implementation- didefinisikan.Jadi jika Anda gagal menginisialisasi a
bool
, atau jika Anda berhasil menimpanya melalui beberapa pointer dari tipe yang berbeda, maka asumsi kompiler akan salah dan Perilaku Tidak Terdefinisi akan terjadi. Anda telah diperingatkan:sumber
true
Nilai tidak harus sama dengan integer 1" agak menyesatkan. Tentu, pola bit yang sebenarnya bisa menjadi sesuatu yang lain, tetapi ketika secara implisit dikonversi / dipromosikan (satu-satunya cara Anda akan melihat nilai selaintrue
/false
),true
selalu1
, danfalse
selalu0
. Tentu saja, kompiler seperti itu juga tidak akan dapat menggunakan trik yang coba digunakan oleh kompiler ini (menggunakan fakta bahwabool
pola bit aktual hanya bisa0
atau1
), jadi agak tidak relevan dengan masalah OP.true
ke pola bit1
, itu hak prerogatifnya. Jika ia memilih beberapa set representasi lain, maka memang tidak bisa menggunakan optimasi yang disebutkan di sini. Jika ia memilih representasi tertentu, maka ia bisa. Itu hanya perlu konsisten secara internal. Anda dapat memeriksa representasi daribool
dengan menyalinnya ke dalam array byte; itu bukan UB (tapi itu adalah implementasi yang ditentukan)bool
pola bit yang dimiliki0
atau1
. Mereka tidak booleanize abool
setiap kali mereka membacanya dari memori (atau register yang memiliki fungsi arg). Itulah yang dikatakan jawaban ini. contoh : gcc4.7 + dapat mengoptimalkanreturn a||b
untukor eax, edi
dalam suatu fungsi kembalibool
, atau MSVC dapat mengoptimalkana&b
untuktest cl, dl
. x86'stest
adalah bitwiseand
, jadi jikacl=1
dandl=2
uji set flag menurutcl&dl = 0
.Fungsi itu sendiri sudah benar, tetapi dalam program pengujian Anda, pernyataan yang memanggil fungsi menyebabkan perilaku tidak terdefinisi dengan menggunakan nilai variabel yang tidak diinisialisasi.
Bug ada di fungsi panggilan, dan itu bisa dideteksi oleh tinjauan kode atau analisis statis dari fungsi panggilan. Menggunakan tautan penjelajah kompiler Anda, kompiler gcc 8.2 mendeteksi bug. (Mungkin Anda bisa mengajukan laporan bug terhadap dentang yang tidak menemukan masalah).
Perilaku tidak terdefinisi berarti apa pun dapat terjadi, termasuk program menabrak beberapa baris setelah peristiwa yang memicu perilaku tidak terdefinisi.
NB. Jawaban untuk "Bisakah perilaku tidak terdefinisi menyebabkan _____?" selalu "Ya". Itulah definisi perilaku yang tidak terdefinisi.
sumber
bool
pemicu yang tidak diinisialisasi ke UB?bool
). Menyalin memerlukan evaluasi sumbertrue
,false
dannot-a-thing
nilai untuk boolean.Sebuah bool hanya diperbolehkan untuk memegang nilai-nilai dependen implementasi yang digunakan secara internal untuk
true
danfalse
, dan kode yang dihasilkan dapat mengasumsikan bahwa ia hanya akan menampung satu dari dua nilai ini.Biasanya, implementasi akan menggunakan integer
0
untukfalse
dan1
untuktrue
, untuk menyederhanakan konversi antarabool
danint
, dan membuatif (boolvar)
menghasilkan kode yang sama denganif (intvar)
. Dalam hal itu, orang dapat membayangkan bahwa kode yang dihasilkan untuk ternary dalam penugasan akan menggunakan nilai sebagai indeks menjadi array pointer ke dua string, yaitu mungkin dikonversi menjadi sesuatu seperti:Jika
boolValue
tidak diinisialisasi, sebenarnya bisa menyimpan nilai integer apa pun, yang kemudian akan menyebabkan akses di luar batasstrings
array.sumber
bool
untukint
dengan*(int *)&boolValue
dan mencetaknya untuk tujuan debugging, lihat apakah itu selain0
atau1
ketika crash. Jika itu masalahnya, itu cukup banyak mengkonfirmasi teori bahwa kompiler mengoptimalkan inline-jika sebagai array yang menjelaskan mengapa ia crash.std::bitset<8>
tidak memberi saya nama yang bagus untuk semua flag saya yang berbeda. Tergantung pada apa mereka, itu mungkin penting.Merangkum pertanyaan Anda banyak, Anda bertanya Apakah standar C ++ memungkinkan kompiler untuk menganggap a
bool
hanya dapat memiliki representasi numerik internal '0' atau '1' dan menggunakannya sedemikian rupa?Standar tidak mengatakan apa-apa tentang representasi internal a
bool
. Itu hanya mendefinisikan apa yang terjadi ketika castingbool
keint
(atau sebaliknya). Sebagian besar, karena konversi integral ini (dan fakta bahwa orang-orang sangat bergantung pada mereka), kompiler akan menggunakan 0 dan 1, tetapi tidak harus (meskipun harus menghormati kendala dari setiap ABI tingkat bawah yang digunakannya. ).Jadi, kompiler, ketika melihat a
bool
berhak untuk mempertimbangkan bahwa kata tersebutbool
mengandung salah satu dari pola bit 'true
' atau 'false
' dan melakukan apa pun rasanya. Jadi jika nilai-nilai untuktrue
danfalse
yang 1 dan 0, masing-masing, compiler memang diperbolehkan untuk mengoptimalkanstrlen
untuk5 - <boolean value>
. Perilaku menyenangkan lainnya dimungkinkan!Seperti yang berulang kali dinyatakan di sini, perilaku tidak terdefinisi memiliki hasil yang tidak ditentukan. Termasuk tetapi tidak terbatas pada
Lihat Apa yang harus diketahui setiap programmer tentang perilaku tidak terdefinisi
sumber