Tidak semua yang berbeda antar compiler adalah undefined behavior. Standar C++ juga menyerahkan sebagian keputusan kepada implementasi — compiler dan platform tempat kode dikompilasi — dengan syarat keputusan itu didokumentasikan. Kelompok ini disebut implementation-defined behavior, dan ia bukan bug: programnya sah, perilakunya pasti, hanya saja pasti di platform ini dan bisa berbeda di platform lain. Chapter ini memetakan tiga tingkat ketidakpastian standar, menunjukkan cara membaca keputusan implementasi langsung dari kode, lalu merangkum kebiasaan yang membuat kode tetap portabel.
Tiga Tingkat Ketidakpastian #
Standar mengenal tiga istilah yang sering dipakai tertukar. Perbedaannya menentukan seberapa besar kita boleh bergantung pada hasilnya:
| Sifat | Standar menjamin | Contoh | Risiko keamanan |
|---|---|---|---|
| Implementation-defined | Ada satu perilaku pasti, dipilih implementasi dan didokumentasikan | sizeof(int), signedness char, shift kanan nilai negatif | logika yang menyandarkan diri pada satu pilihan pecah saat porting |
| Unspecified | Salah satu dari beberapa perilaku sah, tanpa kewajiban dokumentasi | urutan evaluasi argumen f(a(), b()) | hasil bisa berubah antar compiler, bahkan antar panggilan |
| Undefined | Tidak ada jaminan apa pun | luapan int, akses memori ilegal | apa saja — termasuk celah eksploitasi |
Catatan pentingnya: implementation-defined itu sah dan biasanya cepat — compiler memilih apa yang terbaik untuk platformnya. Masalah baru muncul ketika kita diam-diam menyandarkan logika pada satu pilihan, misalnya menganggap char selalu bertanda atau int selalu 4 byte, tanpa pemeriksaan apa pun.
Membaca Keputusan Implementasi di Platform Ini #
Karena setiap keputusan wajib didokumentasikan, kita juga bisa menanyakannya langsung dari kode — semuanya operasi terdefinisi, tanpa sedikit pun UB. Program berikut memeriksa platform dengan sizeof, std::numeric_limits, std::endian dari header <bit>, dan satu kasus shift kanan nilai negatif:
// platform-cek.cpp — membaca keputusan implementasi di platform ini, semuanya terdefinisi.
#include <bit>
#include <iostream>
#include <limits>
int main() {
std::cout << "sizeof(char) = " << sizeof(char) << " byte\n";
std::cout << "sizeof(int) = " << sizeof(int) << " byte\n";
std::cout << "sizeof(long) = " << sizeof(long) << " byte\n";
std::cout << "sizeof(void*) = " << sizeof(void*) << " byte\n";
std::cout << "char bertanda? = " << std::boolalpha
<< std::numeric_limits<char>::is_signed << "\n";
std::cout << "urutan byte = "
<< (std::endian::native == std::endian::little ? "little-endian" : "besar atau lain")
<< "\n";
int negatif = -8;
std::cout << "-8 >> 1 (di GCC) = " << (negatif >> 1) << "\n";
return 0;
}Output yang Dihasilkan:
sizeof(char) = 1 byte
sizeof(int) = 4 byte
sizeof(long) = 8 byte
sizeof(void*) = 8 byte
char bertanda? = true
urutan byte = little-endian
-8 >> 1 (di GCC) = -4Bedah baris-per-baris:
- Baris 8–11 (
sizeof(...)) —sizeofadalah ekspresi konstanta yang nilainya implementation-defined; di platform ini (x86-64, GCC 14)longdan pointer berukuran 8 byte. Di Windows 64-bit,longbernilai 4 byte — contoh klasik beda implementasi. - Baris 13–14 (
is_signed) — standar mengizinkancharbertanda atau tidak; GCC di x86-64 memilih bertanda (true), ARM biasanya tidak. Kode yang menyimpan byte non-ASCII dalamcharlalu membandingkannya akan berperilaku berbeda di sana. - Baris 16–18 (
std::endian) — konstantastd::endian::nativedibandingkan denganstd::endian::littlesaat kompilasi. Di platform ini hasilnya little-endian; kode serialisasi yang menyimpan byte mentah tanpa format eksplisit akan rusak saat dibaca di mesin big-endian. - Baris 20–21 (
-8 >> 1) — shift kanan nilai negatif adalah implementation-defined; GCC mendokumentasikan shift aritmetika (tanda tetap dipertahankan), sehingga hasilnya -4. Compiler lain boleh memilih shift logika yang menghasilkan angka besar positif.
Nilai-nilai di atas deterministik untuk platform course ini, tetapi jangan pernah menyalinnya ke kode sebagai angka pasti — selalu tanyakan lewat sizeof, std::numeric_limits, atau std::endian sehingga program tetap benar di platform lain.
Kebiasaan Menulis Kode Portabel #
Portabilitas adalah penerapan langsung dari pemetaan di atas. Pertama, jangan menghitung ukuran dengan asumsi — minta ke sizeof dan std::numeric_limits, atau pakai tipe lebar tetap <cstdint> seperti std::int32_t bila ukuran memang bagian dari kontrak (format berkas, protokol jaringan). Kedua, jangan menyandarkan logika pada signedness char: bila byte diperlakukan sebagai angka, simpan di unsigned char atau std::byte. Ketiga, untuk data yang melintasi batas mesin, tetapkan format eksplisit (urutan byte, panjang field) dan konversi sendiri — jangan menulis byte mentah dari int dan berharap mesin pembaca punya keputusan implementasi yang sama.
Keempat, waspadai kasus unspecified: urutan evaluasi argumen cetak(hitungA(), hitungB()) tidak dijamin, sehingga fungsi dengan efek samping yang saling bergantung bisa menghasilkan urutan berbeda antar build. Aturan praktisnya sederhana: pisahkan efek samping ke pernyataan tersendiri, sehingga urutan menjadi tidak relevan.
Program aman di atas juga memperlihatkan pola pertahanan yang akan kita pakai sepanjang section ini: alih-alih berasumsi, tanyakan; alih-alih mengandalkan keberuntungan, periksa. Pertanyaan berikutnya lebih tajam lagi: apa yang terjadi ketika program membaca dan menulis memori di luar area yang menjadi haknya — akses di luar batas.
Referensi: cppreference — sizeof , cppreference — std::endian , cppreference — std::numeric_limits .