Arti sebenarnya "undefined behavior"
Halaman sebelumnya menunjukkan bagaimana try/catch menangani error yang didefinisikan dan sengaja dilempar oleh programmu. Undefined behavior adalah kebalikannya: kumpulan operasi yang sama sekali tidak diberi arti oleh standar C++. Tidak ada exception yang bisa ditangkap, tidak ada kode error, tidak ada jaminan program akan crash. Compiler bebas menganggap UB tidak pernah terjadi dan bebas melakukan apa saja saat UB terjadi.
Kebebasan itulah yang membuat UB begitu berbahaya. Baris bermasalah yang sama bisa mencetak jawaban yang "benar" di laptopmu, mengembalikan nilai sampah di server, dan dihapus seluruhnya oleh optimizer di -O2. UB bukan "perilaku yang tidak kami dokumentasikan", tapi "perilaku yang sama sekali tidak dijanjikan bahasa ini". Tugasmu adalah tidak pernah menulisnya sejak awal.
int arr[3] = {1, 2, 3};
int x = arr[5]; // undefined behavior: reading past the end of the array
Tidak ada error kompilasi di sini, dan di banyak percobaan program ini akan diam-diam memberimu integer acak. Keberhasilan semu itulah jebakannya.
Membaca atau menulis di luar batas
Bentuk UB yang paling umum adalah menyentuh memori yang bukan milikmu. Array bawaan dan std::vector::operator[] tidak mengecek batas sama sekali: indeks yang melewati ujung (atau indeks negatif) langsung menjadi UB, baik saat membaca maupun menulis.
Bug yang perlu diwaspadai adalah <= padahal maksudmu <: saat i == v.size(), kamu mengakses satu posisi melewati elemen terakhir, dan itu UB. Pilih for berbasis range (sudah dibahas sebelumnya) kalau kamu tidak butuh indeksnya, karena loop itu tidak bisa berjalan melewati ujung. Kalau kamu memang mengakses indeks secara manual dan ingin jaring pengaman, v.at(i) melempar std::out_of_range alih-alih diam-diam merusak memori:
Pakai at() saat kamu sedang memburu bug; kembali ke [] di loop yang performanya kritis setelah kamu yakin indeksnya valid.
Dangling pointer dan use-after-free
Pointer atau reference yang hidup lebih lama daripada objek yang ditunjuknya disebut dangling. Memakainya adalah UB: memorinya mungkin sudah dipakai ulang, sudah dibebaskan, atau tidak pernah ada. Inilah jebakan yang bisa dihindari dengan smart pointer (dari bab sebelumnya), tapi raw pointer masih bisa menjerumuskanmu ke sana.
Versi yang paling tajam adalah mengembalikan alamat dari variabel lokal. Variabel lokal itu mati saat fungsinya selesai, jadi pemanggilnya hanya memegang pointer ke ketiadaan:
int* makeNumber() {
int n = 42;
return &n; // returns address of a local - it's gone after return
}
// Dereferencing the result is undefined behavior.
Hal yang sama terjadi setelah delete, atau saat vector dialokasikan ulang sehingga iterator atau pointer ke dalamnya menjadi tidak valid:
int* p = new int(5);
delete p;
cout << *p; // use-after-free: undefined behavior
vector<int> v = {1, 2, 3};
int* first = &v[0];
v.push_back(4); // may reallocate - 'first' is now dangling
cout << *first; // undefined behavior
Pertahanannya sudah kamu kenal: jaga objek tetap hidup selama masih ada pointer yang membutuhkannya, pilih reference dan smart pointer daripada raw pointer yang memiliki objek, dan ambil ulang pointer/iterator setelah setiap operasi yang bisa mengubah ukuran container.
Variabel yang belum diinisialisasi dan signed overflow
Membaca variabel sebelum kamu memberinya nilai adalah UB untuk tipe bawaan: tidak ada nilai default 0. Variabel itu berisi bit apa pun yang sudah ada di memori itu, dan optimizer boleh menganggap kamu tidak pernah membacanya sebelum diinisialisasi.
Kalau sum dideklarasikan sebagai int sum; saja, setiap sum += i akan membaca nilai yang tidak tentu lebih dulu. Itu UB, dan bug yang terkenal sulit dilacak karena sering kelihatan berjalan. Biasakan selalu menginisialisasi: int x = 0; atau int x{};.
Pelaku diam-diam lainnya adalah signed integer overflow. Mendorong int yang signed melewati nilai maksimumnya adalah UB (tipe unsigned berputar dengan cara yang bisa ditebak; tipe signed tidak):
int big = 2147483647; // INT_MAX on a 32-bit int
int oops = big + 1; // signed overflow: undefined behavior
Jangan mengandalkan nilainya "berputar menjadi angka negatif". Compiler boleh menganggap overflow tidak mungkin terjadi dan bisa melakukan optimasi berdasarkan anggapan itu. Kalau kamu butuh perputaran yang terdefinisi, pakai tipe unsigned atau cek batasnya sebelum menjumlahkan.
Menangkap UB dengan sanitizer dan peringatan
Kamu tidak bisa yakin bebas dari UB hanya dengan menjalankan tes, karena program yang berhasil dijalankan tidak menjamin apa pun. Yang benar-benar berhasil adalah membuat UB terdengar jelas saat runtime dengan sanitizer milik compiler (tersedia di GCC dan Clang).
// AddressSanitizer: out-of-bounds, use-after-free, leaks
g++ -fsanitize=address -g -O1 main.cpp -o app && ./app
// UndefinedBehaviorSanitizer: signed overflow, null deref, bad casts
g++ -fsanitize=undefined -g main.cpp -o app && ./app
Jalankan tes-tes yang sudah kamu punya dengan flag ini, maka pembacaan di luar batas, use-after-free, atau signed overflow yang tadinya "berjalan lancar" berubah menjadi laporan tepat yang menyebut nama file dan barisnya. Gabungkan dengan -Wall -Wextra supaya compiler juga menandai kode yang mencurigakan (seperti kemungkinan membaca variabel yang belum diinisialisasi) bahkan sebelum kamu menjalankannya.
==1234==ERROR: AddressSanitizer: heap-use-after-free on address 0x...
READ of size 4 at 0x... thread T0
#0 main.cpp:7 in main
Perlakukan setiap laporan sanitizer sebagai bug yang wajib diperbaiki, bukan peringatan yang boleh diabaikan. Laporan itu memberi tahu bahwa standar tidak menjanjikan apa pun tentang baris tersebut.
Rangkuman
Undefined behavior adalah bagian C++ tempat pagar pengaman dilepas: akses di luar batas, dangling pointer, use-after-free, pembacaan variabel yang belum diinisialisasi, dan signed overflow semuanya menghasilkan kode tanpa arti yang terdefinisi, dan "programnya jalan kok" tidak pernah membuktikan bahwa kodenya benar. Cara untuk tetap aman adalah menulis kode secara defensif (inisialisasi setiap variabel, hormati batas container, biarkan smart pointer memiliki memori heap-mu), lalu verifikasi dengan -fsanitize=address, -fsanitize=undefined, dan -Wall -Wextra supaya UB yang diam-diam berubah menjadi laporan yang jelas dan bisa diperbaiki.
Itulah penutup bab Error & Debugging. Dengan exception, try/catch, dan rasa waspada yang sehat terhadap UB, sekarang kamu punya alat untuk menulis C++ yang gagal dengan jelas dan sengaja, bukan diam-diam dan tanpa sengaja.
Pertanyaan yang sering diajukan
Apa itu undefined behavior di C++?
Undefined behavior (UB) adalah operasi apa pun yang secara eksplisit dibiarkan standar C++ tanpa hasil yang terdefinisi, misalnya membaca melewati ujung array atau melakukan dereference pada dangling pointer. Compiler boleh melakukan apa saja: crash, mengembalikan nilai sampah, menghapus kode itu saat optimasi, atau kelihatan berjalan hari ini lalu rusak setelah dikompilasi ulang. Ini bug di programmu, bukan fitur bahasa.
Kenapa program C++-ku berjalan padahal ada undefined behavior?
"Programnya jalan kok" tidak membuktikan apa pun soal UB. Standar tidak memberi jaminan apa pun, jadi bug UB bisa menghasilkan hasil yang kamu harapkan di komputermu dengan compiler-mu hari ini, lalu crash di level optimasi, platform, atau versi compiler yang berbeda. Jangan pernah menganggap program yang berhasil dijalankan sebagai bukti bahwa UB tidak berbahaya. Pakai sanitizer untuk benar-benar menangkapnya.
Bagaimana cara menangkap undefined behavior di C++?
Kompilasi dengan sanitizer: -fsanitize=address (AddressSanitizer) menemukan baca/tulis di luar batas dan use-after-free, sedangkan -fsanitize=undefined (UndefinedBehaviorSanitizer) menandai signed overflow, dereference null, dan cast yang salah. Aktifkan peringatan (-Wall -Wextra) dan jalankan tes-tesmu dengan flag ini, maka UB yang diam-diam berubah menjadi laporan runtime yang jelas.