C++ mengizinkan banyak operasi yang hasilnya “tidak terdefinisi” — kompiler bebas mengasumsikan operasi tersebut tidak pernah terjadi dan melakukan optimisasi yang dapat menghasilkan jawaban acak, crash, atau bahkan kode yang tidak pernah dieksekusi. Inilah undefined behavior (UB): program tidak salah secara sintaksis tetapi perilakunya tidak terjamin. Pada Chapter ini kita akan membedah lima jenis UB yang paling sering muncul di kode DSA — signed overflow, null deref, out-of-bounds, uninitialized read, dan use-after-free — beserta versi amannya. Tujuan akhirnya: mengenali pola UB pada pandangan pertama dan menulis kode yang bebas UB sejak awal.
Aturan: Apa Itu Undefined Behavior? #
Standar C++ mendefinisikan UB sebagai “perilaku untuk program yang salah secara semantik yang tidak diharuskan didiagnosis”. Implikasinya:
- Kompiler boleh mengasumsikan UB tidak pernah terjadi — dan mengoptimisasi berdasarkan asumsi tersebut.
- Hasil bisa benar pada satu compiler, salah pada compiler lain, benar di debug build, salah di optimized build.
- UB tidak selalu crash — sering “berjalan baik” selama bertahun-tahun sampai input tertentu memicunya.
Lima UB Umum di Kode DSA #
| # | Jenis UB | Penyebab Tipikal | Cara Mendeteksi | Cara Menghindari |
|---|---|---|---|---|
| 1 | Signed integer overflow | a + b, a * b melebihi INT_MAX | UBSan | Pakai long long atau cek rentang |
| 2 | Null pointer dereference | *p saat p == nullptr | UBSan | Cek nullptr sebelum deref |
| 3 | Out-of-bounds access | v[i] saat i >= size() | ASan | Cek i < size() |
| 4 | Uninitialized read | Baca variabel sebelum assign | MSan | Selalu inisialisasi eksplisit |
| 5 | Use-after-free | *p setelah delete p | ASan | Pakai std::unique_ptr / std::shared_ptr |
Mekanisme: Mengapa UB Berbahaya #
Compiler modern melakukan optimisasi berbasis asumsi “UB tidak terjadi”. Contoh klasik:
int foo(int x) {
if (x + 100 < x) return 1; // signed overflow -> UB
return 0;
}Compiler berasumsi x + 100 < x selalu false (karena signed overflow UB, kompiler boleh mengasumsikan tidak terjadi). Maka seluruh if dihapus — foo selalu mengembalikan 0. Padahal, untuk x = INT_MAX - 50, programmer mungkin mengharapkan 1. Inilah bahaya UB: kode “terlihat benar” tetapi dioptimisasi menjadi sesuatu yang lain.
Selain itu, UB sering tidak deterministik — bila ada data race, urutan evaluasi, atau sign-extension yang berbeda antar build, hasil bisa bervariasi. Untuk DSA kompetitif, ini berarti kode bisa Accepted di lokal tetapi Wrong Answer di judge karena optimisasi berbeda.
Kode: Lima UB dan Versi Aman #
#include <iostream>
#include <vector>
#include <climits>
#include <memory>
// UB 1: Signed integer overflow
// Versi aman: cast ke long long sebelum operasi.
long long safe_multiply(int a, int b) {
return (long long)a * (long long)b;
}
// UB 2: Null pointer dereference
// Versi aman: cek nullptr sebelum dereference.
int safe_deref(const int* p) {
return p ? *p : -1;
}
// UB 3: Out-of-bounds array access
// Versi aman: cek indeks terhadap size.
int safe_index(const std::vector<int>& v, size_t i) {
return (i < v.size()) ? v[i] : -1;
}
// UB 4: Uninitialized read
// Versi aman: selalu inisialisasi.
int safe_init() {
int x = 0;
return x;
}
// UB 5: Use-after-free
// Versi aman: pakai std::unique_ptr (RAII).
int safe_unique_ptr() {
auto p = std::make_unique<int>(42);
int v = *p;
// unique_ptr otomatis delete saat scope exit. Tidak ada use-after-free.
return v;
}
void demo_signed_overflow_behavior() {
// Unsigned overflow adalah well-defined (wrap-around modular 2^N).
unsigned int u = UINT_MAX;
std::cout << "unsigned UINT_MAX + 1 = " << (u + 1) << " (wrap-around well-defined)\n";
int s = INT_MAX;
std::cout << "INT_MAX = " << s << '\n';
std::cout << "safe_multiply(INT_MAX, 2) = " << safe_multiply(s, 2) << '\n';
}
int main() {
std::cout << "=== Lima UB Umum dan Versi Aman ===\n\n";
std::cout << "1) Signed integer overflow\n";
demo_signed_overflow_behavior();
std::cout << '\n';
std::cout << "2) Null pointer dereference\n";
int x = 99;
std::cout << " safe_deref(&x) = " << safe_deref(&x) << '\n';
std::cout << " safe_deref(nullptr) = " << safe_deref(nullptr) << '\n';
std::cout << '\n';
std::cout << "3) Out-of-bounds array access\n";
std::vector<int> v = {10, 20, 30};
std::cout << " safe_index(v, 0) = " << safe_index(v, 0) << '\n';
std::cout << " safe_index(v, 5) = " << safe_index(v, 5) << " (out-of-bounds)\n";
std::cout << '\n';
std::cout << "4) Uninitialized variable read\n";
std::cout << " safe_init() = " << safe_init() << " (selalu 0 — tidak ada UB)\n";
std::cout << '\n';
std::cout << "5) Use-after-free\n";
std::cout << " safe_unique_ptr() = " << safe_unique_ptr()
<< " (RAII membersihkan otomatis)\n";
std::cout << '\n';
std::cout << "=== Tabel Ringkasan UB ===\n";
std::cout << "Jenis UB | Penyebab | Cara Hindari\n";
std::cout << "------------------------|-------------------------------|---------------------------\n";
std::cout << "Signed integer overflow | a+b, a*b melebihi INT_MAX | Pakai long long / cek dulu\n";
std::cout << "Null deref | *p saat p == nullptr | Cek nullptr sebelum deref\n";
std::cout << "Out-of-bounds | a[i] saat i >= size | Cek i < size() sebelum akses\n";
std::cout << "Uninitialized read | int x; (tanpa = nilai) | Selalu inisialisasi eksplisit\n";
std::cout << "Use-after-free | *p setelah delete p | Pakai unique_ptr / shared_ptr\n";
return 0;
}Output yang Dihasilkan:
=== Lima UB Umum dan Versi Aman ===
1) Signed integer overflow
unsigned UINT_MAX + 1 = 0 (wrap-around well-defined)
INT_MAX = 2147483647
safe_multiply(INT_MAX, 2) = 4294967294
2) Null pointer dereference
safe_deref(&x) = 99
safe_deref(nullptr) = -1
3) Out-of-bounds array access
safe_index(v, 0) = 10
safe_index(v, 5) = -1 (out-of-bounds)
4) Uninitialized variable read
safe_init() = 0 (selalu 0 — tidak ada UB)
5) Use-after-free
safe_unique_ptr() = 42 (RAII membersihkan otomatis)
=== Tabel Ringkasan UB ===
Jenis UB | Penyebab | Cara Hindari
------------------------|-------------------------------|---------------------------
Signed integer overflow | a+b, a*b melebihi INT_MAX | Pakai long long / cek dulu
Null deref | *p saat p == nullptr | Cek nullptr sebelum deref
Out-of-bounds | a[i] saat i >= size | Cek i < size() sebelum akses
Uninitialized read | int x; (tanpa = nilai) | Selalu inisialisasi eksplisit
Use-after-free | *p setelah delete p | Pakai unique_ptr / shared_ptrBedah Kode Baris-per-Baris #
return (long long)a * (long long)b;: cast kelong longsebelum perkalian. UntukINT_MAX * 2 ≈ 4.3 × 10^9yang melebihiINT_MAX, hasilnya disimpan sebagailong longtanpa overflow. Tanpa cast, perkalianint * intdilakukan dalamintdulu — overflow sebelum cast.return p ? *p : -1;: cekptruthy (bukan nullptr) sebelum dereference. Tanpa cek,*nullptradalah segfault atau UB. Pada 64-bit, null pointer deref sering crash, tetapi UB dapat juga menghasilkan data acak.return (i < v.size()) ? v[i] : -1;: cek indeks sebelum akses.std::vector::operator[]tidak melakukan bounds checking — akses OOB adalah UB.std::vector::at(i)melakukan cek dan melemparstd::out_of_range, tetapi dengan overhead.int x = 0;: inisialisasi eksplisit. Variabel lokalint x;tanpa inisialisasi berisi nilai tak terduga dari memori stack — UB bila dibaca. Compile dengan-Wuninitializeduntuk warning.auto p = std::make_unique<int>(42);: smart pointer RAII. Memori dibebaskan otomatis saatpkeluar scope. Tidak adadeletemanual, sehingga tidak ada kemungkinan lupa delete atau double-delete.unsigned int u = UINT_MAX; (u + 1): unsigned overflow adalah well-defined — wrap-around modular2^32. Berbeda dengan signed overflow yang UB. Untuk algoritma hash atau modular arithmetic, gunakanunsigneddengan sengaja.
UB Tambahan yang Sering Muncul di DSA #
Selain lima di atas, beberapa UB lain yang sering ditemui:
| UB | Contoh | Konsekuensi |
|---|---|---|
| Shift berlebih | 1 << 32 (untuk int 32-bit) | Hasil tak terdefinisi; biasanya 0 atau 1 |
| Integer division by zero | 10 / 0 | Crash (SIGFPE) |
| Signed shift negatif | -1 >> 1 (impl. defined) | Hasil bervariasi |
| Stricter aliasing | cast int* ke float* lalu deref | Optimisasi menghasilkan kode salah |
| Multi-modification | i = i++ + ++i; | Hasil tak terdefinisi (urutan evaluasi) |
Invalid memcpy overlap | memcpy(a, a+1, n) | UB; gunakan memmove |
Strategi Deteksi UB #
Ringkasan #
- UB adalah perilaku tidak terdefinisi — compiler boleh mengasumsikan UB tidak terjadi dan mengoptimisasi berdasarkan asumsi tersebut.
- Lima UB umum di DSA: signed overflow, null deref, out-of-bounds, uninitialized read, use-after-free.
- Versi aman:
long longuntuk operasi besar, cek nullptr, cek bounds, inisialisasi eksplisit,unique_ptr/shared_ptruntuk manajemen memori. - Unsigned overflow adalah well-defined (wrap-around) — berguna untuk hash dan modular arithmetic.
- Strategi deteksi:
-Wall -Wextra -Werroruntuk warning statis,-fsanitize=address,undefineduntuk deteksi runtime. - “Berjalan baik di mesin saya” bukan bukti kode bebas UB — selalu uji dengan sanitizer sebelum submit.