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:

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

text
suhu     = 36.7
dipotong = 36
kode     = 321
kecil    = 'A' (kode karakter 65)
besar    = 3000000000
dipangkas = -1294967296

Baris demi baris yang menjelaskan kenapa hasilnya bisa jauh dari nilai asal:

  1. Baris 9-10 (double suhu = 36.7; int dipotong = suhu;): bagian pecahan .7 dibuang, bukan dibulatkan, sehingga 36.7 menjadi 36. Inilah truncation yang sudah kita lihat pada Chapter Konversi Tipe Data — kini dalam bentuk implisit yang tidak meninggalkan jejak di kode.
  2. Baris 12-13 (int kode = 321; char kecil = kode;): char hanya menyimpan satu byte (8 bit), sedangkan 321 butuh sembilan bit. Yang dipertahankan hanya delapan bit terbawah: 321 dalam biner adalah 1 0100 0001, sehingga yang tersisa 0100 0001 — desimal 65, dan karakter 65 adalah 'A'. Nilai 321 berubah menjadi sesuatu yang sama sekali lain.
  3. Baris 15-16 (long besar = 3'000'000'000L; int dipangkas = besar;): pada platform kita long berukuran 64 bit dan int 32 bit. Nilai 3.000.000.000 melebihi batas int (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.
  4. Baris 21-22 (static_cast<int>(kecil)): konversi ini justru aman karena char ke int selalu menampung tanpa kehilangan; kita memakai static_cast hanya 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:

cpp brace-demo.cpp — tiga penulisan yang ditolak compiler
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:

text
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:

KonversiApa yang terjadiContoh hasil
double → intpecahan dibuang, bukan dibulatkan36.7 → 36
double → floatdigit di luar presisi float dibulatkan0.1 (double) → 0.1f yang tidak persis sama
long → intbit di luar 32 bit dibuang; tanda bisa berubah3.000.000.000 → -1294967296
int → char/shorthanya bit terbawah yang dipertahankan321 → 'A' (65)
int → unsigned intbilangan negatif menjadi nilai modulo 2³²-1 → 4294967295
std::size_t → intukuran container dipangkas ke tipe bertandarisiko 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 =:

text
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.