Dua chapter terakhir meninggalkan satu janji yang belum ditepati. Chapter Konversi Tipe Data menegaskan bahwa konversi yang membuang data sebaiknya selalu tertulis, dan Chapter Inisialisasi Variabel menyinggung bahwa kurung kurawal punya pengaman ekstra untuk menolak konversi berisiko. Chapter ini menuntaskan keduanya lewat satu istilah: narrowing conversion — konversi dari tipe yang lebih lebar ke tipe yang lebih sempit, sehingga ada kemungkinan nilai berubah. Kita mulai dari versi paling berbahayanya, yaitu narrowing yang lolos begitu saja dari compiler, lalu melihat bagaimana kurung kurawal mengubahnya menjadi error saat kompilasi, dan menutup dengan static_cast sebagai cara menyatakan pemotongan yang memang disengaja.
Narrowing yang Lolos dari Compiler #
Narrowing tidak pernah memunculkan error pada bentuk inisialisasi =. Compiler menerima baris int dipotong = suhu; dengan senyap, sebab aturan bahasanya memang mengizinkan konversi implisit tersebut — padahal nilainya bisa saja berubah. Program berikut memuat tiga kasus klasik: double ke int, int ke char, dan long ke int, semuanya dengan sumber berupa variable yang nilainya baru diketahui saat program berjalan:
// narrowing.cpp — implicit narrowing yang lolos dari compiler.
// Ketiga baris di bawah memotong nilai tanpa error dan tanpa peringatan
// (-Wall -Wextra), karena sumbernya variable yang baru diketahui saat
// program berjalan. Bandingkan dengan brace-demo yang ditolak compiler.
#include <iostream>
int main() {
double suhu = 36.7;
int dipotong = suhu; // double -> int: pecahan .7 dibuang
int kode = 321;
char kecil = kode; // int -> char: hanya 8 bit terbawah
long besar = 3'000'000'000L;
int dipangkas = besar; // long -> int: melewati batas int
std::cout << "suhu = " << suhu << "\n";
std::cout << "dipotong = " << dipotong << "\n";
std::cout << "kode = " << kode << "\n";
std::cout << "kecil = '" << kecil << "' (kode karakter "
<< static_cast<int>(kecil) << ")\n";
std::cout << "besar = " << besar << "\n";
std::cout << "dipangkas = " << dipangkas << "\n";
return 0;
}Output yang Dihasilkan:
suhu = 36.7
dipotong = 36
kode = 321
kecil = 'A' (kode karakter 65)
besar = 3000000000
dipangkas = -1294967296Baris demi baris yang menjelaskan kenapa hasilnya bisa jauh dari nilai asal:
- Baris 9-10 (
double suhu = 36.7; int dipotong = suhu;): bagian pecahan.7dibuang, bukan dibulatkan, sehingga36.7menjadi36. Inilah truncation yang sudah kita lihat pada Chapter Konversi Tipe Data — kini dalam bentuk implisit yang tidak meninggalkan jejak di kode. - Baris 12-13 (
int kode = 321; char kecil = kode;):charhanya menyimpan satu byte (8 bit), sedangkan 321 butuh sembilan bit. Yang dipertahankan hanya delapan bit terbawah:321dalam biner adalah1 0100 0001, sehingga yang tersisa0100 0001— desimal 65, dan karakter 65 adalah'A'. Nilai321berubah menjadi sesuatu yang sama sekali lain. - Baris 15-16 (
long besar = 3'000'000'000L; int dipangkas = besar;): pada platform kitalongberukuran 64 bit danint32 bit. Nilai 3.000.000.000 melebihi batasint(2.147.483.647), sehingga bit di luar 32 bit dibuang dan sisanya “dibungkus” menjadi -1.294.967.296 — angka besar berubah menjadi angka negatif. - Baris 21-22 (
static_cast<int>(kecil)): konversi ini justru aman karenacharkeintselalu menampung tanpa kehilangan; kita memakaistatic_casthanya agar kode karakternya tampil sebagai angka, bukan sebagai simbol.
Tiga baris konversi di atas semuanya lolos kompilasi dengan -Wall -Wextra tanpa satu pun peringatan. Inilah sifat narrowing yang paling perlu kita waspadai: compiler tidak menolaknya, dan sebagian besar bahkan tidak memperingatkannya.
Kurung Kurawal Menolak Narrowing #
Bentuk inisialisasi kurung kurawal dari Chapter Inisialisasi Variabel ternyata punya tugas khusus di sini: list initialization menolak setiap konversi yang tergolong narrowing. Kita uji dengan tiga baris yang nilainya persis sama dengan program di atas, kini ditulis dengan kurung kurawal dan disimpan sebagai brace-demo.cpp:
int dipotong{36.7};
char kecil{321};
int dipangkas{3'000'000'000L};
int main() {
return 0;
}Kompilasi file tersebut gagal total. Pesan galat berikut adalah hasil nyata dari g++ 14.2:
brace-demo.cpp:4:14: error: narrowing conversion of ‘3.6700000000000003e+1’ from ‘double’ to ‘int’ [-Wnarrowing]
4 | int dipotong{36.7};
| ^~~~
brace-demo.cpp:5:12: error: narrowing conversion of ‘321’ from ‘int’ to ‘char’ [-Wnarrowing]
5 | char kecil{321};
| ^~~
brace-demo.cpp:6:15: error: narrowing conversion of ‘3000000000’ from ‘long int’ to ‘int’ [-Wnarrowing]
6 | int dipangkas{3'000'000'000L};
| ^~~~~~~~~~~~~~Setiap baris mendapat satu error narrowing conversion ... [-Wnarrowing], lengkap dengan nilai asal dan pasangan tipe yang bertengkar. Bandingkan dengan versi = yang lewat tanpa suara — perbedaan perilakunya terjadi murni karena bentuk penulisan, bukan karena nilainya berubah. Dengan kurung kurawal, kesalahan seperti salah menaksir rentang tipe langsung tertangkap saat kompilasi, jauh sebelum program sempat menjalankan perhitungan yang salah.
Perhatikan bahwa pengecualian itu hanya berlaku untuk konstanta. Begitu sumbernya variable — seperti char kecil = kode; pada program pertama — compiler tidak bisa menjamin nilainya muat, dan kurung kurawal menolak apa pun bentuk penulisannya. Kebijakan yang konsisten inilah yang membuat kurung kurawal aman dijadikan gaya bawaan.
Daftar Kasus Narrowing yang Umum #
Tabel berikut merangkum konversi yang tergolong narrowing dan pola kerusakan nilainya:
| Konversi | Apa yang terjadi | Contoh hasil |
|---|---|---|
double → int | pecahan dibuang, bukan dibulatkan | 36.7 → 36 |
double → float | digit di luar presisi float dibulatkan | 0.1 (double) → 0.1f yang tidak persis sama |
long → int | bit di luar 32 bit dibuang; tanda bisa berubah | 3.000.000.000 → -1294967296 |
int → char/short | hanya bit terbawah yang dipertahankan | 321 → 'A' (65) |
int → unsigned int | bilangan negatif menjadi nilai modulo 2³² | -1 → 4294967295 |
std::size_t → int | ukuran container dipangkas ke tipe bertanda | risiko index negatif pada file besar |
Dua baris terakhir patut digarisbawahi karena sering muncul tanpa disadari. Konversi int ke unsigned sebenarnya perilakunya terdefinisi rapi, tetapi hasilnya hampir pasti bukan yang dimaksud — latar belakangnya sudah kita bedah pada Chapter Signed dan Unsigned. Sedangkan std::size_t yang dikonversi diam-diam ke int adalah sumber bug klasik pada kode yang memproses container besar.
Kapan Narrowing Berbahaya #
Narrowing menjadi masalah nyata di tiga situasi. Pertama, saat nilai berasal dari luar kendali kita — masukan pengguna, ukuran berkas, hasil sensor — yang rentangnya tidak kita kenali saat menulis kode. Kedua, saat konversi terjadi di tengah ekspresi panjang, sehingga siapa pun yang membaca kode mudah melewatkannya. Ketiga, saat pemotongan berlangsung berulang di loop: kehilangan satu digit mungkin tak terlihat, tetapi penumpukannya merusak data secara diam-diam.
Menariknya, compiler sebenarnya bisa menolong pada satu kasus: bila sumbernya konstanta yang sudah terlihat saat kompilasi. Uji kecil berikut, disimpan sebagai char-demo.cpp, memicu peringatan meskipun ditulis dengan =:
char-demo.cpp: In function ‘int main()’:
char-demo.cpp:2:18: warning: overflow in conversion from ‘int’ to ‘char’ changes value from ‘300’ to ‘44’ [-Woverflow]
2 | char tetap = 300; // konstanta diketahui compiler: muncul peringatan
| ^~~
char-demo.cpp:2:10: warning: unused variable ‘tetap’ [-Wunused-variable]
2 | char tetap = 300; // konstanta diketahui compiler: muncul peringatan
| ^~~~~Nilai 300 memang tidak muat di char, compiler melihatnya langsung, dan -Woverflow memberi tahu hasil pemotongannya (44). Tetapi perhatikan batas penolong ini: begitu sumbernya variable — sebagaimana ketiga baris pada narrowing.cpp — peringatan yang sama tidak muncul sama sekali. Ada juga flag khusus bernama -Wconversion yang melaporkan hampir semua narrowing, namun dia sering terlalu bising untuk diaktifkan terus-menerus, sehingga timbul pertanyaan: siapa yang menjaga selain compiler?
static_cast sebagai Pernyataan Eksplisit #
Solusi C++ modern membagi tugas di antara dua alat. Kurung kurawal menjadi pilihan bawaan ketika kita tidak ingin ada pemotongan — compiler berdiri di gerbang dan menolak yang berbahaya. Sedangkan saat pemotongan memang disengaja, kita menuliskannya dengan static_cast, misalnya int dipotong = static_cast<int>(suhu);. Compiler menerima baris itu tanpa protes, dan pembaca kode langsung tahu bahwa pecahan yang dibuang adalah keputusan yang disadari, bukan kecerobohan.
Kombinasi keduanya juga rapi: static_cast<int>(36.7) menghasilkan int murni, sehingga boleh ditulis sebagai int dipotong{static_cast<int>(36.7)}; tanpa memicu -Wnarrowing. Pola ini sering kita temui di kode pustaka standar: kurung kurawal menjaga setiap inisialisasi, dan static_cast menandai titik-titik pemotongan yang diperhitungkan.
Setelah urusan konversi tuntas, satu topik menutup section ini. Nama tipe di C++ bisa tumbuh panjang — std::size_t yang barusan kita sebut sebenarnya adalah nama lain untuk tipe unsigned yang lebih panjang — dan menulis nama panjang itu berulang-ulang melelahkan. Chapter berikutnya memperkenalkan type alias dengan using, cara memberi nama sendiri pada tipe apa pun agar kode tetap ringkas dan mudah dibaca.
Rujukan resmi untuk topik Chapter ini: implicit conversions , list initialization , dan explicit type conversion di cppreference.