Integer overflow adalah salah satu bug paling terselubung di C++. Signed overflow (mis. INT_MAX + 1) adalah undefined behavior — compiler boleh mengasumsikan tidak terjadi dan mengoptimalkan berdasarkan asumsi tersebut, menghasilkan kode yang aneh. Unsigned overflow well-defined (wrap modulo 2^N) tetapi tetap bisa menjadi bug logika. Untuk DSA kompetitif, overflow sering menjadi penyebab WA (Wrong Answer) pada test case besar. Pada Chapter ini kita akan membedah signed vs unsigned overflow, pola pengecekan sebelum operasi, penggunaan long long dan __int128 untuk angka besar, serta modular arithmetic dengan mulmod.

Aturan Dasar #

OperasiPerilaku
Signed overflow (int, long long)Undefined behavior — compiler boleh mengasumsikan tidak terjadi
Unsigned overflow (unsigned int, size_t)Well-defined: wrap modulo 2^N
Shift lebih dari bit widthUB
Division/modulo by zeroUB (crash)

Mekanisme: Mengapa Signed Overflow UB #

Compiler modern memakai asumsi “no signed overflow” untuk optimisasi. Misalnya loop for (int i = 0; i <= N; ++i) diasumsikan tidak tak terhingga — compiler boleh hapus pengecekan. Bila overflow sebenarnya terjadi, hasilnya tidak terdefinisi: bisa wrap, bisa crash, bisa menghasilkan nilai aneh.

Sebaliknya, unsigned arithmetic wrap modulo 2^N secara matematis. 0u - 1 == UINT_MAX, 255 + 1 (unsigned char) == 0. Perilaku konsisten di semua platform.

Kode: Deteksi dan Pencegahan #

cpp
#include <iostream>
#include <climits>
#include <cstdint>
#include <string>
#include <algorithm>

// 1) Signed overflow adalah UB — compiler bisa asumsikan tidak terjadi
int naive_mul(int a, int b) {
    return a * b;   // bila a*b > INT_MAX → UB
}

// 2) Pengecekan sebelum operasi — aman
bool safe_mul(int a, int b, int& out) {
    if (a > 0 && b > 0 && a > INT_MAX / b) return false;
    if (a > 0 && b < 0 && b < INT_MIN / a) return false;
    if (a < 0 && b > 0 && a < INT_MIN / b) return false;
    if (a < 0 && b < 0 && a < INT_MAX / b) return false;  // neg * neg
    // a == 0 atau b == 0 → hasil 0, aman
    out = a * b;
    return true;
}

// 3) Unsigned overflow — well-defined (wrap modulo 2^N)
void demo_unsigned_wrap() {
    unsigned int u = 0;
    std::cout << "u = " << u << ", u - 1 = " << (u - 1) << " (wrap to UINT_MAX)\n";

    unsigned char c = 255;
    c = c + 1;   // assignment kembali ke unsigned char — wrap to 0
    std::cout << "255 + 1 (unsigned char) = " << (int)c << " (wrap to 0)\n";
}

// 4) Cast ke wider type untuk cek
bool safe_add_ll(long long a, long long b, long long& out) {
    if (b > 0 && a > LLONG_MAX - b) return false;
    if (b < 0 && a < LLONG_MIN - b) return false;
    out = a + b;
    return true;
}

// 5) __int128 untuk big number (GCC extension)
void demo_int128() {
    __int128 big = (__int128)1 << 100;   // 2^100
    __int128 two_60 = (__int128)1 << 60;
    __int128 prod = big * 1;             // tetap aman

    // Cetak __int128 manual (tidak ada iostream support)
    auto print128 = [](__int128 v) {
        if (v == 0) { std::cout << '0'; return; }
        bool neg = v < 0;
        if (neg) v = -v;
        std::string s;
        while (v > 0) { s += char('0' + (int)(v % 10)); v /= 10; }
        if (neg) s += '-';
        std::reverse(s.begin(), s.end());
        std::cout << s;
    };
    std::cout << "2^100 = ";
    print128(big);
    std::cout << "\n2^60  = ";
    print128(two_60);
    std::cout << "\nprod  = ";
    print128(prod);
    std::cout << '\n';
}

// 6) Modular arithmetic — pencegahan overflow dengan mod
const long long MOD = 1'000'000'007LL;

long long mulmod(long long a, long long b, long long m) {
    // a, b < m < 2^63 → a*b bisa overflow long long
    // Solusi 1: __int128
    return (long long)((__int128)a * b % m);
}

long long mulmod_safe(long long a, long long b, long long m) {
    // Solusi 2: russian peasant — tanpa __int128
    a %= m; b %= m;
    long long res = 0;
    while (b > 0) {
        if (b & 1) {
            res += a;
            if (res >= m) res -= m;
        }
        a <<= 1;
        if (a >= m) a -= m;
        b >>= 1;
    }
    return res;
}

int main() {
    // Naive (UB bila overflow)
    int x = INT_MAX;
    std::cout << "INT_MAX = " << x << '\n';
    std::cout << "naive_mul(INT_MAX, 2) = " << naive_mul(x, 2) << " (UB!)\n";

    // Safe check
    int out;
    if (safe_mul(x, 2, out)) {
        std::cout << "safe_mul ok: " << out << '\n';
    } else {
        std::cout << "safe_mul: overflow detected\n";
    }
    if (safe_mul(3, 4, out)) {
        std::cout << "safe_mul(3,4) = " << out << '\n';
    }

    // Unsigned wrap
    demo_unsigned_wrap();

    // 64-bit add
    long long lo;
    if (safe_add_ll(LLONG_MAX, 1, lo)) std::cout << "add ok: " << lo << '\n';
    else                                std::cout << "LLONG_MAX + 1 overflow\n";

    // __int128
    demo_int128();

    // Modular arithmetic
    long long a = 1'000'000'000LL, b = 1'000'000'000LL;
    std::cout << "mulmod(" << a << ", " << b << ", " << MOD << ") = "
              << mulmod(a, b, MOD) << '\n';
    std::cout << "mulmod_safe = " << mulmod_safe(a, b, MOD) << '\n';

    // Demo: signed overflow UB (sengaja — pragma untuk sembunyikan warning demo)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Woverflow"
    int big_mul = 100000 * 100000;   // 10^10 — overflow int!
#pragma GCC diagnostic pop
    std::cout << "100000 * 100000 (int)   = " << big_mul << " (WRONG!)\n";
    long long big_mul_ll = 100000LL * 100000LL;
    std::cout << "100000LL * 100000LL     = " << big_mul_ll << " (correct)\n";

    return 0;
}

Output yang Dihasilkan:

text
INT_MAX = 2147483647
naive_mul(INT_MAX, 2) = -2 (UB!)
safe_mul: overflow detected
safe_mul(3,4) = 12
u = 0, u - 1 = 4294967295 (wrap to UINT_MAX)
255 + 1 (unsigned char) = 0 (wrap to 0)
LLONG_MAX + 1 overflow
2^100 = 1267650600228229401496703205376
2^60  = 1152921504606846976
prod  = 1267650600228229401496703205376
mulmod(1000000000, 1000000000, 1000000007) = 49
mulmod_safe = 49
100000 * 100000 (int)   = 1410065408 (WRONG!)
100000LL * 100000LL     = 10000000000 (correct)

Bedah Kode Baris-per-Baris #

  1. naive_mul(INT_MAX, 2): operasi INT_MAX * 2 adalah signed overflow → UB. Output -2 adalah salah satu kemungkinan (compiler wrap dua’s complement), tetapi tidak terjamin. Bisa jadi crash, hasil lain, atau di-optimasi menjadi apa pun.
  2. safe_mul(INT_MAX, 2, out): cek sebelum operasi — a > INT_MAX / b yaitu INT_MAX > INT_MAX / 2 ≈ 1.07 miliar. Karena INT_MAX lebih besar, return false. Implementasi safe_mul memeriksa empat kasus tanda (positif×positif, positif×negatif, negatif×positif, negatif×negatif) — masing-masing dengan batas berbeda.
  3. safe_mul(3, 4, out): cek lolos (3 <= INT_MAX/4), out = 12.
  4. u - 1 dengan u = 0 (unsigned): well-defined wrap modulo 2^32. Hasilnya 4294967295 == UINT_MAX. Tidak UB.
  5. unsigned char c = 255; c = c + 1;: c + 1 dipromosikan ke int (256), lalu di-assign ke unsigned char — modulo 2^8 menjadi 0. Wrap terdefinisi.
  6. safe_add_ll(LLONG_MAX, 1, ...): cek a > LLONG_MAX - b → LLONG_MAX > LLONG_MAX - 1 → true → return false. Overflow dideteksi.
  7. __int128 big = (__int128)1 << 100: cast 1 ke __int128 sebelum shift, sehingga shift << 100 valid (128 bit). Tanpa cast, 1 << 100 pada int adalah UB (shift > 31).
  8. Lambda print128: __int128 tidak didukung iostream — harus cetak manual. Loop modulo 10, simpan digit, lalu reverse. Mendukung nilai negatif.
  9. mulmod(1e9, 1e9, 1e9+7): 1e9 * 1e9 = 1e18 — overflow long long? Tidak, LLONG_MAX ≈ 9.2e18. Tetapi 1e9 * 1e9 + ε bisa overflow. Pakai __int128 untuk aman: hasil 1e18 mod (1e9+7) = 49.
  10. mulmod_safe: algoritma russian peasant (binary multiplication). b di-shift ke kanan, a di-shift ke kiri (dua kali lipat). Bila bit b adalah 1, tambahkan a ke res. Semua operasi modulo m — tidak ada overflow.
  11. 100000 * 100000 (int): 10^10 overflow int (max ~2.1 miliar). Hasil 1410065408 (≈ 10^10 mod 2^32 dalam interpretasi dua’s complement). Bug terselubung.
  12. 100000LL * 100000LL: literal LL membuat operasi di long long. Hasil 10^10 benar.

Pola Pengecekan Sebelum Operasi #

Untuk a + b (signed): if (b > 0 && a > MAX - b) overflow; if (b < 0 && a < MIN - b) overflow;

Untuk a * b (signed): empat kasus berdasarkan tanda a dan b. Lebih sederhana: cek if (a != 0 && (b > MAX/a || b < MIN/a)).

Untuk a - b: if (b > 0 && a < MIN + b) overflow; if (b < 0 && a > MAX + b) overflow;.

Alternatif: cast ke tipe lebih lebar (long long untuk int, __int128 untuk long long) dan cek rentang. Lebih sederhana tetapi tidak portable untuk tipe terbesar.

__int128 untuk Big Number #

GCC/Clang menyediakan __int128 — 128-bit integer. Range ~-1.7×10^38 hingga +1.7×10^38. Mendukung aritmetika dasar tetapi tidak ada dukungan iostream — harus cetak manual.

Penggunaan umum di DSA: modular multiplication (a*b mod m dengan a, b, m < 2^63), Fibonacci ke-100, perhitungan kombinasi besar.

Ringkasan #

  • Signed overflow = UB; unsigned overflow = wrap modulo 2^N.
  • Compiler boleh mengoptimasi berasumsi signed overflow tidak terjadi — perilaku tidak terjamin.
  • Pola cek sebelum operasi: untuk +/-/×, periksa rentang hasil sebelum eksekusi.
  • Selalu tulis literal LL/ULL untuk angka besar — 1000000LL * 1000000LL bukan 1000000 * 1000000.
  • Untuk modular multiplication, pakai __int128 atau algoritma russian peasant untuk menghindari overflow long long.
  • __int128 (GCC/Clang) untuk angka hingga ~10^38 — butuh cetak manual karena iostream tidak mendukung.
  • Untuk angka > 10^38, gunakan pustaka bignum (boost::multiprecision).