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 UBPenyebab TipikalCara MendeteksiCara Menghindari
1Signed integer overflowa + b, a * b melebihi INT_MAXUBSanPakai long long atau cek rentang
2Null pointer dereference*p saat p == nullptrUBSanCek nullptr sebelum deref
3Out-of-bounds accessv[i] saat i >= size()ASanCek i < size()
4Uninitialized readBaca variabel sebelum assignMSanSelalu inisialisasi eksplisit
5Use-after-free*p setelah delete pASanPakai std::unique_ptr / std::shared_ptr

Mekanisme: Mengapa UB Berbahaya #

Compiler modern melakukan optimisasi berbasis asumsi “UB tidak terjadi”. Contoh klasik:

cpp
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 #

cpp
#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:

text
=== 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_ptr

Bedah Kode Baris-per-Baris #

  1. return (long long)a * (long long)b;: cast ke long long sebelum perkalian. Untuk INT_MAX * 2 ≈ 4.3 × 10^9 yang melebihi INT_MAX, hasilnya disimpan sebagai long long tanpa overflow. Tanpa cast, perkalian int * int dilakukan dalam int dulu — overflow sebelum cast.
  2. return p ? *p : -1;: cek p truthy (bukan nullptr) sebelum dereference. Tanpa cek, *nullptr adalah segfault atau UB. Pada 64-bit, null pointer deref sering crash, tetapi UB dapat juga menghasilkan data acak.
  3. 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 melempar std::out_of_range, tetapi dengan overhead.
  4. int x = 0;: inisialisasi eksplisit. Variabel lokal int x; tanpa inisialisasi berisi nilai tak terduga dari memori stack — UB bila dibaca. Compile dengan -Wuninitialized untuk warning.
  5. auto p = std::make_unique<int>(42);: smart pointer RAII. Memori dibebaskan otomatis saat p keluar scope. Tidak ada delete manual, sehingga tidak ada kemungkinan lupa delete atau double-delete.
  6. unsigned int u = UINT_MAX; (u + 1): unsigned overflow adalah well-defined — wrap-around modular 2^32. Berbeda dengan signed overflow yang UB. Untuk algoritma hash atau modular arithmetic, gunakan unsigned dengan sengaja.

UB Tambahan yang Sering Muncul di DSA #

Selain lima di atas, beberapa UB lain yang sering ditemui:

UBContohKonsekuensi
Shift berlebih1 << 32 (untuk int 32-bit)Hasil tak terdefinisi; biasanya 0 atau 1
Integer division by zero10 / 0Crash (SIGFPE)
Signed shift negatif-1 >> 1 (impl. defined)Hasil bervariasi
Stricter aliasingcast int* ke float* lalu derefOptimisasi menghasilkan kode salah
Multi-modificationi = i++ + ++i;Hasil tak terdefinisi (urutan evaluasi)
Invalid memcpy overlapmemcpy(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 long untuk operasi besar, cek nullptr, cek bounds, inisialisasi eksplisit, unique_ptr/shared_ptr untuk manajemen memori.
  • Unsigned overflow adalah well-defined (wrap-around) — berguna untuk hash dan modular arithmetic.
  • Strategi deteksi: -Wall -Wextra -Werror untuk warning statis, -fsanitize=address,undefined untuk deteksi runtime.
  • “Berjalan baik di mesin saya” bukan bukti kode bebas UB — selalu uji dengan sanitizer sebelum submit.