- Aturan bahasa C dapat membuat kode yang tampak sederhana sekalipun menjadi perilaku tak terdefinisi, misalnya pada perbandingan pointer, aliasing, null pointer, dan nilai yang belum diinisialisasi
- Konstanta integer,
sizeof, konstanta karakter, dan aritmetika uint8_t dapat menghasilkan nilai berbeda tergantung platform, penulisan, dan lokasi penugasan antara karena pemilihan tipe dan promosi integer
foo() dan foo(void) dalam deklarasi fungsi, ketiadaan prototipe, promosi argumen bawaan, dan fungsi tanpa nilai balik memiliki legalitas maupun perilaku yang berbeda antara C dan C++
- Array bukan pointer, parameter array disesuaikan menjadi pointer, dan
a, &a, &a[0] tidak bisa saling dipertukarkan walaupun alamatnya sama karena tipenya berbeda
- Prioritas operator dan urutan evaluasi adalah hal yang berbeda; termasuk struktur isi
switch dan masa hidup objek sementara, rumusan standar menentukan hasil eksekusi nyata
Perilaku tak terdefinisi dan aturan pointer
-
Perbandingan pointer dan aturan strict aliasing
- Walaupun pointer bertipe sama
p dan q menunjuk ke alamat yang sama, jika keduanya berasal dari objek yang berbeda dan bukan bagian dari objek aggregate atau union yang sama, maka perbandingan p == q bisa menjadi perilaku tak terdefinisi
- Gagasan bahwa pointer lebih abstrak daripada sekadar alamat numerik dilanjutkan dalam artikel terkait
- Mengakses objek
int melalui lvalue short menjadi perilaku tak terdefinisi menurut aturan strict aliasing
- Pointer
unsigned char adalah pengecualian yang dapat mengalias objek apa pun, sehingga mengakses objek int melalui lvalue unsigned char adalah sah
unsigned char dijamin tidak memiliki padding bit maupun trap representation, dan sejak C11, signed char juga dijamin tidak memiliki padding bit
- Analisis aliasing berbasis tipe dibahas dalam artikel terkait
-
Null pointer dan representasi pointer
- Representasi bit dari null pointer tidak harus berupa semua bit 0
- Standar C mendefinisikan null pointer constant, tetapi tidak mendefinisikan representasi null pointer saat runtime maupun representasi pointer umum
- Symbolics Lisp Machine 3600 menggunakan tuple berbentuk
<array-object, index> alih-alih pointer numerik, dan representasi null pointer-nya adalah <nil, 0>
- Contoh tambahan ada di clc FAQ 5.17
- Konstanta
0 dapat menjadi integer atau null pointer tergantung konteks, dan (void *)0 dievaluasi sebagai null pointer
- Hanya karena ekspresi
e bernilai 0, tidak berarti (void *)e dijamin menjadi null pointer
- Hanya ketika null pointer constant dikonversi ke tipe pointer, hasilnya dijamin sama dengan null pointer
- Aritmetika pada null pointer adalah perilaku tak terdefinisi, jadi walaupun
e adalah null pointer, e + 0 tidak dijamin tetap null pointer
-
Nilai yang belum diinisialisasi
- Saat membaca objek dengan durasi penyimpanan otomatis yang belum diinisialisasi, jika objek tersebut bisa memiliki kelas penyimpanan
register dan alamatnya belum pernah diambil, maka menurut C11 § 6.3.2.1 ¶ 2 hal itu menjadi perilaku tak terdefinisi
- Aturan ini terkait dengan arsitektur Intel Itanium yang dibahas dalam DR338
- Register integer umum pada Itanium memiliki 64 bit dan satu trap bit, dan trap bit ini adalah
NaT (not-a-thing) yang menunjukkan apakah register telah diinisialisasi
- Jika alamat variabel diambil, kondisi tersebut hilang, tetapi nilainya tetap indeterminate dan bisa berupa trap representation atau unspecified value
- Membaca trap representation menjadi perilaku tak terdefinisi menurut C11 § 6.2.6.1 ¶ 5
- Jika nilainya unspecified, hasil
x != x bisa saja true maupun false, dan bila int x bersifat unspecified, bahkan setelah x *= 0 pun x tidak dijamin bernilai 0
- Indeterminate dan unspecified value dibahas dalam DR260, DR451, N1793, N1818, N2012, N2013, N2221
-
unsigned char dan memcpy
- Tipe
unsigned char tidak memiliki trap representation menurut C11 § 6.2.6.1 ¶ 3, sehingga nilai awalnya bersifat unspecified
- Jawaban dari anggota komite C di StackOverflow menyatakan bahwa setelah pemanggilan fungsi pustaka standar
memcpy, nilai x seharusnya menjadi specified, dan dalam tafsiran ini x != x akan menjadi false
- Namun dasar yang mendukung hal ini dalam standar C tidak jelas, dan tanggapan komite pada DR451 menyatakan bahwa menggunakan fungsi pustaka pada indeterminate value merupakan perilaku tak terdefinisi, sehingga bertentangan dengan tafsiran tersebut
- Pertanyaan ini tetap terbuka, dan diskusi tambahan tersedia di Uninitialized Reads
Konstanta integer, promosi, sizeof
-
Notasi dan tipe konstanta integer
- Konstanta integer desimal tanpa sufiks selalu dipilih dari daftar tipe signed, sedangkan konstanta oktal dan heksadesimal dapat bertipe signed atau unsigned
- Menurut C17 § 6.4.4.1, tipe konstanta integer ditentukan sebagai tipe pertama dalam daftar yang dapat merepresentasikan nilai tersebut
- Saat tidak ada sufiks, konstanta desimal mengikuti urutan
int, long int, long long int, sedangkan konstanta oktal dan heksadesimal mengikuti urutan int, unsigned int, long int, unsigned long int, long long int, unsigned long long int
- Konstanta di antara
INT_MAX+1 hingga UINT_MAX dapat memiliki tipe berbeda tergantung apakah ditulis dalam desimal atau heksadesimal, dan ini bisa menimbulkan perbedaan pada kode yang sensitif terhadap ABI seperti pemanggilan fungsi variadik
- Dalam Arm 32-bit architecture ABI,
int dan long dikirim sebagai 32 bit dalam satu register, sedangkan long long dikirim sebagai 64 bit dalam dua register
- Pada platform dengan
int 32 bit, -1 < 0x8000 akan bernilai true, sedangkan pada platform dengan int 16 bit akan bernilai false, sehingga dapat menimbulkan masalah portabilitas
- Perbedaan tipe konstanta juga dapat mengubah hasil pada ekspresi seperti generic selection, fungsi overload C++, dan
sizeof(0x80000000) == sizeof(2147483648)
-
sizeof(int) > -1
- Operator
sizeof mengembalikan bilangan bulat unsigned bertipe size_t
- Menurut usual arithmetic conversions pada C11 § 6.3.1.8, jika operan signed memiliki rank lebih rendah daripada operan unsigned, maka ia dikonversi ke tipe unsigned dengan rank yang sama
- Saat dikonversi ke unsigned, integer signed yang merepresentasikan
-1 akan menjadi integer unsigned maksimum pada rank tersebut
- Karena itu,
sizeof(int) > -1 selalu dievaluasi sebagai false
-
Tipe konstanta karakter
- Dalam C, konstanta karakter bertipe
int menurut C11 § 6.4.4.4 ¶ 10
- Karena itu, tidak ada jaminan bahwa
sizeof(char) == sizeof('x') selalu true; yang dijamin hanyalah sizeof(int) == sizeof('x')
- Integer character constant dapat berupa satu atau lebih urutan karakter multibyte, sehingga
'abc' juga valid, dan representasinya ditentukan oleh implementasi
- Nilai integer character constant yang berisi satu karakter sama dengan representasi integer dari objek bertipe
char yang merepresentasikan karakter tunggal yang sama
-
Aritmetika uint8_t dan pembagian
- Meskipun
a, b, dan c sudah diinisialisasi sebelum dibaca, nilai x dan z bisa berbeda karena promosi integer dan lokasi penugasan perantara
- Nilai tiap variabel dipromosikan ke ukuran
int, lalu penjumlahan dan pembagian dilakukan, dan hasil setiap penugasan disimpan setelah ditrunkasi ke tipe variabel terkait
- Sebagai contoh, jika
a=255, b=1, c=2, maka x menjadi ((255 + 1) / 2) % 256 = 128
- Variabel perantara
y menjadi (255 + 1) % 256 = 0, lalu z menjadi (0 / 2) % 256 = 0, sehingga 128 != 0
- Overflow integer unsigned adalah perilaku yang terdefinisi
- Karena operasi modulo bersifat distributif terhadap penjumlahan, jika pembagian diganti dengan penjumlahan maka
x dan z akan selalu sama
- Jika penugasan pertama diubah menjadi
uint8_t x = ((uint8_t)(a + b)) / c;, maka x dan z juga akan selalu sama
-
Variabel const dan variable length array
- Walaupun variabel
n dan m dibatasi dengan const dan digunakan sebagai ukuran array, keduanya bukan integer constant expression dalam C
- Pada C11 § 6.6 ¶ 6, integer constant expression dibatasi pada integer constant, enumeration constant, character constant,
sizeof, _Alignof, hasil cast yang bernilai integer constant, floating constant yang menjadi operan langsung, dan bentuk sejenis lainnya
- Jika ekspresi ukuran array bukan integer constant expression, maka menurut C11 § 6.7.6.2 ¶ 4 array tersebut menjadi variable length array
- Variable length array tidak diizinkan pada file scope, sehingga compilation unit yang memiliki array global
x tidak akan bisa dikompilasi
- Pada block scope, variable length array diizinkan, sehingga compilation unit yang memiliki array lokal
y dapat dikompilasi
- Variable length array adalah conditional feature yang implementasinya boleh tidak didukung, sehingga pada compiler yang tidak mendukungnya, contoh block scope pun mungkin tidak dapat dikompilasi
- Dalam C++, kedua compilation unit dapat dikompilasi, dan karena C++ tidak memiliki konsep variable length array,
y akan dikompilasi sebagai array biasa dengan 42 elemen
Deklarasi fungsi, nilai kembalian, linkage
-
foo() dan foo(void)
- Deklarasi fungsi berbentuk
foo() menyatakan fungsi yang jumlah dan tipe argumennya tidak diketahui, sedangkan foo(void) menyatakan nullary function yang tidak memiliki argumen
- Perbedaan ini dibahas dalam artikel tentang deklarasi, definisi, dan prototipe fungsi
- Deklarasi tanpa daftar argumen hanya memperkenalkan nama fungsi dan tidak mendefinisikan jumlah maupun tipe argumennya, sehingga bisa legal jika dipadukan dengan definisi fungsi di bagian berikutnya
- Jika fungsi dipanggil tanpa prototipe, default argument promotions diterapkan sehingga
float dipromosikan menjadi double
- Jika tipe fungsi setelah promosi tidak kompatibel dengan tipe pada definisi fungsi yang sebenarnya, maka kombinasi deklarasi dan definisi tersebut tidak valid
- Pemanggilan fungsi tanpa deklarasi dapat dikompilasi di C karena fungsi implisit diizinkan, tetapi di C++ hal itu adalah galat kompilasi
- Jika memanggil seperti
bar(42) tanpa deklarasi, promosi argumen bilangan bulat diterapkan sehingga 42 direpresentasikan sebagai int, jadi jika bar tidak kompatibel dengan T (*)(int) untuk suatu tipe kembalian T, maka perilakunya tidak terdefinisi
-
Fungsi value-returning yang tidak mengembalikan nilai
- Fungsi dengan tipe kembalian
int tetap bisa legal di C meskipun tidak mengembalikan nilai, selama nilai hasil pemanggilan tidak digunakan
- K&R C tidak memiliki tipe
void, dan jika tipe dihilangkan maka tipe baku int diasumsikan, sehingga fungsi yang tidak mengembalikan nilai dan aturan int implisit secara historis saling terkait
- Aturan
int implisit dihapus pada C99, dan pembahasan terkait ada di N661 dan C99 rationale
- C17 § 6.9.1 ¶ 12 menetapkan bahwa jika eksekusi mencapai
} di akhir fungsi dan pemanggil menggunakan nilai hasil pemanggilan fungsi, maka itu adalah perilaku tidak terdefinisi
- Dalam C++98 § 6.6.3 ¶ 2, keluar begitu saja dari akhir fungsi value-returning itu sendiri setara dengan
return tanpa nilai, dan pada fungsi value-returning hal itu menjadi perilaku tidak terdefinisi
- Kompiler C++ umumnya tidak dapat membuktikan bahwa
abort_program() berakhir pada cabang tertentu, sehingga dalam kasus seperti ini biasanya hanya dapat mengeluarkan diagnosis, bukan galat
-
linkage dan extern
- Jika dalam scope tempat deklarasi sebelumnya terlihat, identifier yang sama dideklarasikan ulang dengan
extern, maka linkage deklarasi yang belakangan sama dengan linkage deklarasi sebelumnya
- C17 § 6.2.2 ¶ 4 menetapkan bahwa jika deklarasi sebelumnya menentukan internal atau external linkage, maka deklarasi
extern berikutnya juga memiliki linkage yang sama
- Jika deklarasi sebelumnya tidak terlihat atau deklarasi sebelumnya tidak memiliki linkage, maka identifier
extern memiliki external linkage
- Kombinasi deklarasi dengan urutan sebaliknya dapat menjadi perilaku tidak terdefinisi, dan GCC serta Clang dapat mendeteksinya
Qualifier dan tipe tidak lengkap
-
const pada parameter fungsi
- Dalam deklarasi fungsi, parameter
x boleh diberi qualifier const, sementara pada definisi fungsi tidak, dan tetap legal meskipun isi fungsi menuliskan nilai ke x
- Menurut C11 § 6.7.6.3 ¶ 15, saat menilai kompatibilitas tipe parameter fungsi dan composite type, setiap parameter yang dideklarasikan dengan qualified type diperlakukan sebagai unqualified version
- Topik yang sama juga dibahas dalam DR040
-
const pada tipe kembalian fungsi
- Jika hanya tipe kembalian pada definisi fungsi yang diberi qualifier
const sedangkan deklarasinya tidak, jawabannya sulit dianggap sekadar benar atau salah
- Kesepakatan umumnya adalah qualifier pada rvalue seharusnya diabaikan, tetapi redaksi standar hingga C11 tidak menanganinya secara eksplisit
- Dalam C17 menjadi jelas bahwa qualifier rvalue harus diabaikan pada cast, lvalue conversion, dan function declarator
- C17 § 6.7.6.3 ¶ 5 menyatakan bahwa tipe yang dikembalikan fungsi adalah unqualified version dari
T, dan redaksi ini ditambahkan di C17
- Penugasan tipe fungsi bisa tetap legal meskipun qualifier
const pada tipe kembaliannya berbeda
- Pembahasan tambahan ada di DR423 dan DR481
-
Struct tidak lengkap dan variabel global
- Pada saat deklarasi variabel global,
struct foo boleh saja masih berupa tipe tidak lengkap sehingga ukurannya belum diketahui, asalkan tipe itu kemudian dilengkapi dalam translation unit yang sama; dalam situasi tertentu hal ini diizinkan
- Logika serupa juga berlaku pada variabel global atau array bertipe tidak lengkap
- Hal ini juga dibahas dalam DR016
-
Objek external bertipe void
- Deklarasi variabel bertipe
void dengan internal linkage tidak legal, tetapi deklarasi variabel bertipe void dengan external linkage legal secara tata bahasa dan tidak secara eksplisit dilarang di mana pun dalam standar C11
- Menurut C11 § 6.2.5 ¶ 19, tipe
void adalah tipe objek tidak lengkap yang tidak dapat dilengkapi yang tersusun dari himpunan kosong nilai
- C11 § 6.3.2.1 ¶ 1 mendefinisikan lvalue sebagai ekspresi bertipe objek selain
void, sehingga nama objek foo bertipe void bukan lvalue yang valid
- Berdasarkan C11, sulit membayangkan operasi yang bermakna dan conforming terhadap objek
void external
- DR012 membahas bahwa jika tipenya diubah menjadi
const void, maka mengambil alamat objek foo menjadi legal, dan ini tampak lebih seperti oversight daripada fitur yang disengaja
-
Konversi pointer-ke-const
Array, string literal, dan penyesuaian pointer
-
Array bukan pointer
- Inisialisasi array dan inisialisasi pointer tidaklah setara
- Bentuk pertama menginisialisasi array yang dapat dimodifikasi dengan durasi penyimpanan otomatis atau statis
- Bentuk kedua menginisialisasi pointer yang menunjuk ke array dengan durasi penyimpanan statis, dan array tersebut belum tentu dapat dimodifikasi
- Array bukanlah pointer, dan rincian lebih lanjut dibahas dalam artikel terkait
-
a, &a, &a[0]
- Pada
int a[42];, a, &a, dan &a[0] semuanya dievaluasi sebagai alamat elemen pertama array
- Namun, tipe ketiga ekspresi itu berbeda satu sama lain, sehingga tidak bisa dipakai saling menggantikan
- Rincian lebih lanjut dibahas dalam artikel terkait
-
Parameter array dan array lokal
- Jika tipe parameter fungsi adalah “array dari
T”, maka tipe itu disesuaikan menjadi “pointer ke T”
- Meskipun parameter
x tampak seperti int[42], sebenarnya ia diperlakukan sebagai int *
- Jika variabel lokal
y adalah int[42], maka sizeof(y) adalah 42 * sizeof(int)
- Secara umum, ukuran pointer objek tidak sama dengan ukuran 42 buah
int, jadi sizeof(x) == sizeof(y) biasanya bernilai false
- Rincian lebih lanjut dibahas dalam artikel terkait
Operator, urutan evaluasi, dan alur kontrol
-
x+++y
- Dalam C, tidak seperti C++, operator baru seperti
+++ tidak bisa didefinisikan, jadi tidak ada operator baru seperti +++
x+++y ditafsirkan sebagai kombinasi operator yang sudah ada, dan setara dengan (x++) + y
--*--p juga bukan operator baru, melainkan kombinasi operator yang sudah ada
--*--p setara dengan --(*(--p)), dan dalam contoh dievaluasi menjadi -1 serta sebagai efek samping menetapkan -1 ke x[0]
-
Urutan evaluasi operand aritmetika
- Prioritas operator terdefinisi dengan baik, tetapi urutan evaluasi operand aritmetika tidak terdefinisi
(x=1) + (x=2) adalah perilaku tak terdefinisi karena urutan kedua penugasan tidak ditentukan, sehingga nilai akhir x tidak pasti 1 atau 2
- Dengan opsi
-std=c11 -O2, GCC 8.2.1 mengevaluasi ekspresi contoh menjadi 4, sedangkan Clang 7.0.0 menjadi 3
-
Urutan evaluasi operator logika
- Pada operator logika
&& dan ||, urutan evaluasi operand juga terdefinisi dengan baik
- Dalam istilah standar C, terdapat sequence point antara evaluasi operand pertama dan evaluasi operand kedua
- Dalam contoh,
x=1 dievaluasi lebih dulu menjadi true, lalu x=2 dievaluasi dan juga menjadi true, sehingga seluruh ekspresi menjadi true
-
Struktur isi switch yang bebas
- Isi pernyataan
switch dapat berupa statement apa pun, sehingga struktur yang mencampurkan loop dan if juga bisa sah
- Bahkan jika itu adalah branch
true di dalam pernyataan if yang ekspresi kontrolnya selalu false, jika ada label case maka statement tersebut menjadi live, dan printf("1"); bukan dead code
- Jika melompat ke
case 2, clause-1 dan ekspresi kontrol loop bisa saja tidak dijalankan, sehingga variabel i harus diinisialisasi terlebih dahulu
- Walaupun terjadi fall through karena tidak ada
break pada case 1, jika case 1 berada di branch true milik if dan case 2 berada di branch false, eksekusi bisa melewati case 2 dan lanjut ke case 3
- Setelah tiga pemanggilan
foo(0); foo(1); foo(2);, output konsol menjadi 02313223
- Contoh nyata terkenal yang mencampurkan loop dan switch adalah Duff's device
Perbedaan umur objek sementara dan versi standar C
- Potongan kode tertentu adalah perilaku tak terdefinisi di C11, tetapi mungkin tidak demikian di C99
- Di C11, umur objek tertentu menjadi lebih pendek, sehingga objek yang dikembalikan oleh pemanggilan fungsi hanya hidup sampai selama operand kanan sedang dievaluasi
- Di C99, objek yang sama hidup sampai akhir enclosing block
- Mereferensikan objek yang umurnya telah berakhir adalah perilaku tak terdefinisi menurut C11 § 6.2.4 ¶ 2
- Bahkan di C99, umur objek dengan automatic storage duration terikat pada enclosing block terdekat, jadi mereferensikan objek itu di luar block tersebut adalah perilaku tak terdefinisi
- C11 § 6.2.4 ¶ 8 menyatakan bahwa non-lvalue expression bertipe struct atau union yang menyertakan array member mereferensikan objek dengan automatic storage duration dan temporary lifetime
- Umur objek sementara ini dimulai saat ekspresi dievaluasi, dan berakhir saat evaluasi full expression atau full declarator yang melingkupinya selesai
- Upaya memodifikasi objek yang memiliki temporary lifetime adalah perilaku tak terdefinisi
- Contoh tersebut diambil dari N1285, dan pembahasan tambahan juga ada di sana
1 komentar
Pendapat dari Lobste.rs
Soal 4 tidak valid di C23, tetapi valid sebelumnya
Soal 10 bukan jawaban benar maupun salah, jadi agak mengganggu untuk disebut pilihan ganda
Soal 15 secara teknis salah, terutama terkait soal 13, dan soal 20 adalah “tidak ditentukan”, jadi lagi-lagi bukan salah satu jawaban
Soal 30 ambigu tergantung cara membacanya
Meski begitu saya tetap menjawab benar 27 dari 31 soal, dan sedikit terbantu karena saya adalah pengembang compiler
Setelah mengerjakan sekitar empat soal, sisa perasaan bahwa C itu sederhana dan layak dipakai untuk side project pun hilang
clang, menggunakan-std=<language-standard>-pedantic -Wall -Wextra, lalu benar-benar memperbaiki setiap warning yang muncul, serta sebisa mungkin menghindari pointer casting dan manipulasi pointer, rasanya jebakan besar bisa dihindariWarning GCC/
clangbelakangan ini cukup bagus, dan untuk <language-standard> bisa memakai c89, c99, c11, c23Jika memakai compiler seperti tcc yang tidak melakukan optimisasi aneh, Anda akan lebih jarang mengalami kejutan ganjil
Saya hanya memilih berdasarkan patokan “perilaku mana yang paling tidak masuk akal di sini?” dan berhasil benar 21 dari 32 soal
Kebanyakan yang salah karena saya tidak memikirkan cukup dalam seberapa tidak masuk akalnya hal itu
Saya cuma sedikit menyentuh C lebih dari 15 tahun lalu, dan melihat kuis seperti ini tidak membuat saya ingin mencobanya lagi
Menurut C23, jawaban untuk soal 4 tidak valid
Menariknya, saya sudah lama tidak memakai C, tetapi tetap menjawab benar 27 dari 32 soal
Hal seperti inilah yang membuat saya selama ini bergantung pada static analyzer dan linter
Saya sudah merasa tidak enak sejak soal 1
Mereka tidak mempertimbangkan dari mana pointer-pointer itu bisa berasal, dan agar kasus yang dibahas di sana bisa berlaku, dibutuhkan kondisi yang sangat khusus
Dalam kebanyakan kasus, mencoba membuat pointer itu sendiri sudah merupakan undefined behavior, tetapi tetap bisa dibilang cukup adil
Soal 3 benar-benar mengejutkan, satu lagi jebakan C
Fakta bahwa literal integer di C sejak awal punya tipe yang sudah pasti itu sangat menjengkelkan
Aturan integer promotion memang sedikit membantu, tetapi juga menjadi sumber bug
Kebanyakan, atau bahkan semua, bahasa modern seharusnya melarang implicit numeric casting, menebak tipe literal dari konteks jika memungkinkan, dan jika tidak memungkinkan maka mewajibkan casting eksplisit
Setelah soal 6 saya berhenti karena tidak percaya lagi pada tesnya
Awalnya karena jawaban soal 5 pada praktiknya tampak dirancang agar membuat orang salah menjawab soal 6, tetapi setelah dilihat lagi sepertinya soal 6 sendiri memang salah
Penjelasannya mengatakan pemanggilan fungsinya adalah undefined behavior, tetapi pertanyaannya menanyakan apakah definisi fungsinya legal, dan kemungkinan besar memang legal
Dan rasanya itu bukan kasus yang terlalu jarang
Soal
switch()benar-benar bagusSulit, tetapi proses memecahkannya di kepala sangat menyenangkan