Setiap tipe integer punya batas — nilai terbesar dan terkecil yang bisa ditampungnya — dan operator aritmetika tidak otomatis menghentikan kita saat hasil melampauinya. Ada dua dunia yang berbeda di sini. Aritmetika unsigned bersifat modular: hasilnya melingkar secara deterministik, maksimum + 1 kembali ke nol. Aritmetika signed yang melampaui batas justru undefined behavior: standar tidak menjamin apa pun, dan compiler berhak mengasumsikan kejadian itu tidak pernah terjadi. Pada Chapter ini kita menguji wrap unsigned yang aman dipamerkan, membahas signed overflow sebagai materi baca (bukan program untuk ditiru), lalu merangkum strategi menghitung dengan aman memakai <limits> dan <cstdint>.
Unsigned: Aritmetika yang Melingkar #
Untuk tipe unsigned, standar mendefinisikan aritmetikanya secara modular: hasil operasi adalah sisa pembagian terhadap maksimum + 1. Perilakunya persis penghitung jarak tempuh (odometer) atau jarum jam yang kembali ke nol setelah angka tertingginya. Karena definisinya eksplisit, wrap unsigned bukan bug — deterministik di semua platform — dan kadang memang dimanfaatkan sengaja, misalnya untuk hashing atau counter melingkar.
#include <cstdint>
#include <iostream>
#include <limits>
int main() {
// Aritmetika unsigned bersifat modular: melingkar secara deterministik
unsigned int nilai = std::numeric_limits<unsigned int>::max();
std::cout << "unsigned int maksimum : " << nilai << "\n";
nilai = nilai + 1u;
std::cout << "maksimum + 1 : " << nilai << "\n";
nilai = 0u - 1u;
std::cout << "0 - 1 : " << nilai << "\n\n";
// Batas tipe dari <limits> dan <cstdint>
std::cout << "INT32_MAX : " << INT32_MAX << "\n";
std::cout << "INT32_MIN : " << INT32_MIN << "\n";
std::cout << "UINT32_MAX : " << UINT32_MAX << "\n\n";
std::cout << "int maksimum : " << std::numeric_limits<int>::max() << "\n";
std::cout << "int minimum : " << std::numeric_limits<int>::min() << "\n";
// Batas diverifikasi pada saat kompilasi
static_assert(UINT32_MAX == 4294967295u);
static_assert(INT32_MAX == 2147483647);
static_assert(sizeof(std::int64_t) >= sizeof(std::int32_t));
return 0;
}Output yang Dihasilkan:
unsigned int maksimum : 4294967295
maksimum + 1 : 0
0 - 1 : 4294967295
INT32_MAX : 2147483647
INT32_MIN : -2147483648
UINT32_MAX : 4294967295
int maksimum : 2147483647
int minimum : -2147483648Mari kita bedah:
- Baris 9-11 (
nilai + 1u):nilaidiisi batas atasunsigned int, yaitu4294967295. Tambah satu melingkar kembali ke0— hasil ini dijamin standar dan sama di semua compiler. - Baris 12-13 (
0u - 1u): arah sebaliknya sama saja,0 - 1mendarat di nilai tertinggi4294967295. Perhatikan literaumenandakan unsigned, sehingga operasinya ikut aturan modular. - Baris 16-18:
INT32_MAX,INT32_MIN, danUINT32_MAXdari header<cstdint>memberi batas tipe berlebar pasti 32 bit — cocok untuk data yang formatnya ditentukan persis, seperti file biner atau protokol jaringan. - Baris 21-22 (
std::numeric_limits<int>):<limits>memberi batas untuk tipe bawaan platform tanpa menebak angka. Nilainya di sini sama dengan 32-bit, tetapi pada platform lainintbisa lebih lebar. - Baris 25-27 (
static_assert): karena batas-batas ini konstanta, compiler bisa memeriksanya saat kompilasi. Pernyataan ini juga menegaskan ulang lebar tipe tetap yang kita pakai.
Signed Overflow: Undefined Behavior #
Sekarang sisi berbahayanya. Untuk tipe signed (int, long long, dan kawan-kawan), hasil yang melampaui batas adalah undefined behavior — bukan nilai tertentu, bukan kembali ke minimum, melainkan tanpa jaminan apa pun. Potongan berikut hanyalah ilustrasi untuk dibaca:
int maksimum = std::numeric_limits<int>::max();
int hasil = maksimum + 1; // UB: tidak ada jaminan apa pun tentang hasil
Mengapa standar memilih jalur berbahaya ini? Karena compiler boleh menganggap signed overflow tidak pernah terjadi, lalu memakai asumsi itu untuk optimasi. Kode pemeriksaan batas seperti if (jumlah + tambahan < jumlah) bisa dihilangkan oleh optimizer karena “perhitungan itu tidak mungkin melampaui batas”. Akibatnya program tidak pernah gagal di tempat yang jelas, tetapi berperilaku aneh di tempat lain — kelas bug yang paling sulit dilacak. Untuk mengamati kejadiannya saat development, kompilasi dengan flag -fsanitize=undefined; UndefinedBehaviorSanitizer akan menghentikan jalannya program dengan pesan seperti:
runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'Pada mesin uji kita, program di atas tampak menghasilkan -2147483648 — persis nilai minimum — tetapi itu perilaku hardware, bukan jaminan standar. Compiler lain atau level optimasi berbeda boleh menghasilkan yang lain. Kalimat kerjanya: jangan pernah membiarkan aritmetika signed mendekati batasnya tanpa pemeriksaan.
Strategi Menghitung dengan Aman #
Ada tiga strategi utama, dan pemilihannya bergantung makna datanya:
- Periksa batas sebelum beroperasi. Sebelum menjumlahkan, uji apakah hasil masih di dalam batas, misalnya menguji
a > batasAtas - bsebelum menghitunga + b. Pola pengecekan seperti ini butuh pernyataan kondisi — kita baru bisa menulisnya rapi setelah Section Control Flow. - Pakai tipe yang lebih lebar. Bila nilai terbesar
std::int32_tterasa sempit, pindah kestd::int64_t;static_assert(sizeof(std::int64_t) >= sizeof(std::int32_t))pada program kita menegaskan lebar minimalnya. Untuk kasus ekstrem, Section Pemrograman Numerik membahas bilangan berukuran bebas. - Pilih unsigned bila maknanya memang melingkar. Hash, checksum, dan counter siklik justru mengandalkan modularitas unsigned. Yang perlu dijauhi adalah mencampur signed dan unsigned dalam satu ekspresi tanpa sadar, karena konversinya otomatis dan bisa mengejutkan.
Mengetahui Batas: <limits> dan <cstdint> #
Dua header di program kita punya peran berbeda. <limits> menyediakan std::numeric_limits<T> yang bekerja untuk semua tipe numerik — termasuk float dan double — sehingga kode tidak menempel angka keras pada satu platform. <cstdint> menyediakan tipe berlebar tetap (std::int32_t, std::uint64_t, dan seterusnya) beserta makro batasnya seperti INT32_MAX; pilihannya tepat saat format data menuntut lebar pasti.
Kombinasi keduanya dengan static_assert memberi lapisan perlindungan yang nyata: compiler menolak program bila asumsi lebar atau batas tipe tidak terpenuhi di platform baru. Kebiasaan kecil ini murah, dan harga bug overflow di produksi jauh lebih mahal.
Dengan topik batas ini, Section Operator tuntas: kita telah menguji seluruh keluarga operator, dari aritmetika hingga cast, lengkap dengan aturan precedence-nya. Berikutnya kita melangkah ke Section Control Flow — di sana operator perbandingan dan logika yang sudah kita kuasai menjadi inti pernyataan if, dan strategi “periksa batas sebelum beroperasi” akhirnya bisa kita tulis sebagai kode sungguhan.