Di Section Polimorfisme kita sudah memakai Box<dyn Trait> untuk menyimpan trait object di heap, tetapi saat itu Box sendiri sengaja tidak kita bedah — Box hanya diposisikan sebagai alat bantu untuk kepemilikan penuh. Sekarang saatnya membuka kap mesinnya. Box<T> adalah smart pointer paling sederhana di Rust: satu struktur seukuran pointer yang mengalokasikan satu nilai T di heap lalu menjadi pemilik tunggal nilai tersebut. Pola “satu pemilik, data di heap” ini menjadi fondasi bagi smart pointer lain yang akan kita temui di sepanjang section ini, mulai dari Rc yang membagikan kepemilikan hingga RefCell yang melonggarkan aturan borrow. Sebelum menyentuh yang rumit, kita perlu memahami tiga hal dasar tentang Box: bagaimana alokasi heap-nya bekerja, bagaimana isinya dibaca dan diubah, dan bagaimana kepemilikannya berpindah.

Masalah: Data yang Ikut Sibuk Berpindah #

Kita mulai dari aturan nyata yang sudah kita kenal. Setiap nilai di Rust hidup di stack atau di heap, dan setiap perpindahan kepemilikan menyalin isi nilai dari satu slot stack ke slot stack lainnya. Untuk nilai kecil seperti i32, penyalinan ini nyaris gratis. Untuk struct besar — misalnya struct laporan dengan banyak field bertipe String, array, dan angka — setiap assignment dan setiap pemanggilan fungsi yang menerima nilai itu utuh menyalin seluruh isi struct. Data String itu sendiri memang tetap satu di heap, tetapi “meja operator” di stack yang menunjuk ke sana ikut tersalin bolak-balik.

Masalah kedua muncul pada tipe yang ukurannya tidak bisa ditentukan saat kompilasi — kita akan bertemu masalah ini secara dramatis pada Chapter berikutnya lewat error E0072. Kedua masalah ini punya satu akar yang sama: nilai terpaksa “berdiri sendiri” di stack dengan ukuran penuh. Solusinya adalah memindahkan nilai ke heap dan meninggalkan satu pointer kecil sebagai perwakilan resminya — dan itulah pekerjaan Box<T>.

Mekanisme Box: Pemilik Seukuran Pointer #

Saat kita menulis Box::new(75), Rust melakukan tiga hal secara berurutan. Pertama, nilai 75 dievaluasi seperti biasa. Kedua, Rust memesan sepetak memori di heap yang cukup untuk menampung nilai bertipe i32. Ketiga, nilai tersebut dipindahkan ke petak heap itu, dan Box::new mengembalikan sebuah Box<i32> berupa pointer menuju petak tersebut. Yang tersimpan di stack hanyalah pointer seukuran satu word — 8 byte pada arsitektur 64-bit — terlepas dari seberapa besar T-nya.

Box juga pemilik yang patuh aturan ownership: saat variabel Box keluar dari scope, Rust menjalankan proses drop atas Box, dan Box membebaskan petak heap yang dikelolanya sekaligus nilai di dalamnya. Karena kepemilikan tunggal, Box tidak butuh penghitung atau pemeriksaan tambahan — pembersihannya pasti terjadi tepat satu kali. Mari kita buktikan dulu klaim “seukuran pointer” dengan pengukuran nyata:

rust
use std::mem::size_of;

fn main() {
    let nilai = Box::new(75);

    println!("nilai di dalam Box: {}", nilai);
    println!("ukuran Box<i32>: {} byte", size_of::<Box<i32>>());
    println!("ukuran i32: {} byte", size_of::<i32>());
}

Output yang Dihasilkan:

text
nilai di dalam Box: 75
ukuran Box<i32>: 8 byte
ukuran i32: 4 byte

Mari kita bedah kode di atas:

  1. Baris 4 (let nilai = Box::new(75);): 75 dipindahkan ke heap dan nilai menjadi variabel stack berisi pointer menuju lokasi heap tersebut. Tipe variabel ini adalah Box<i32>.
  2. Baris 6 (println!("nilai di dalam Box: {}", nilai);): makro println! membaca isi heap secara otomatis tanpa perintah tambahan dari kita — Box menerapkan auto-deref sehingga nilai di dalamnya bisa dibaca seolah-olah nilai itu berada di stack.
  3. Baris 7 (size_of::<Box<i32>>()): pengukuran nyata membuktikan Box<i32> hanya selebar 8 byte, yaitu selebar satu pointer, padahal yang dibungkus cukup besar untuk menampung i32 apa pun.
  4. Baris 8 (size_of::<i32>()): nilai telanjang i32 hanya 4 byte, lebih kecil daripada Box-nya sendiri. Bagi i32, membungkus dengan Box jelas tidak efisien — kita membungkus untuk data besar atau tipe khusus, bukan untuk angka kecil.

Membaca dan Mengubah Isi Box #

Pembacaan lewat println! di atas terasa otomatis, tetapi Rust juga menyediakan operator dereference * untuk mengambil nilai di dalam Box secara eksplisit. Operator inilah yang kita pakai saat mengubah isi Box, dan operator inilah yang bekerja di balik layar pada setiap pemakaian Box yang terasa “transparan”:

rust
fn main() {
    let mut angka = Box::new(75);

    *angka += 25;
    println!("nilai lewat dereference: {}", *angka);
    println!("nilai lewat variabel: {}", angka);
}

Output yang Dihasilkan:

text
nilai lewat dereference: 100
nilai lewat variabel: 100

Mari kita bedah kode di atas:

  1. Baris 2 (let mut angka = Box::new(75);): kata kunci mut wajib ada karena isi heap yang ditunjuk akan diubah. Tanpa mut, kompilator menolak assignment pada baris berikutnya.
  2. Baris 4 (*angka += 25;): operator * mengikuti pointer di stack menuju heap, lalu ekspresi *angka menjadi sebutan langsung untuk nilai i32 di sana. Penjumlahan += 25 mengubah angka 75 menjadi 100 di heap — bukan membuat Box baru.
  3. Baris 5 (println!("...", *angka);): operator * kembali dipakai untuk membaca nilai terbaru secara eksplisit.
  4. Baris 6 (println!("...", angka);): pencetakan tanpa * tetap menghasilkan 100 — makro println! menerima &Box<i32> lalu dereference secara otomatis. Dua baris terakhir ini menunjukkan dua gaya penulisan yang hasilnya identik.

Kemampuan operator * ini bukan sulap khusus Box. Operator tersebut bekerja karena Box mengimplementasikan trait Deref dan DerefMut — mekanisme yang akan kita bedah tuntas pada Chapter Deref dan DerefMut, termasuk bagaimana Box bisa dipakai di banyak tempat tanpa terasa seperti pointer.

Box sebagai Pemilik yang Berpindah #

Box adalah pemilik tunggal, sehingga seluruh aturan move dari Section Ownership berlaku apa adanya. Ketika Box dikirim ke fungsi sebagai nilai, kepemilikan petak heap ikut berpindah, dan pembersihan memori kini menjadi tanggung jawab fungsi penerima:

rust
struct ReportData {
    code: u32,
    summary: String,
}

fn show_report(data: Box<ReportData>) {
    println!("Laporan #{}: {}", data.code, data.summary);
}

fn main() {
    let mut data = Box::new(ReportData {
        code: 107,
        summary: String::from("penjualan kuartal pertama"),
    });

    println!("kode awal: {}", data.code);
    data.code = 110;
    println!("kode baru: {}", data.code);

    show_report(data);
}

Output yang Dihasilkan:

text
kode awal: 107
kode baru: 110
Laporan #110: penjualan kuartal pertama

Mari kita bedah kode di atas:

  1. Baris 1-4 (struct ReportData { ... }): struct biasa dengan dua field — code sebesar 4 byte dan summary berupa String yang menunjuk data teks di heap.
  2. Baris 6 (fn show_report(data: Box<ReportData>)): parameter bertipe Box<ReportData> menyatakan bahwa fungsi ini menerima kepemilikan penuh, bukan pinjaman &. Pemanggil tidak perlu mengembalikan apa pun.
  3. Baris 11-14 (let mut data = Box::new(ReportData { ... });): seluruh struct ReportData dipindahkan ke heap; variabel data di stack hanya menyimpan pointer 8 byte.
  4. Baris 16 (println!("kode awal: {}", data.code);): akses field data.code berjalan langsung tanpa * — operator titik secara otomatis mengikuti pointer Box (auto-deref).
  5. Baris 17 (data.code = 110;): pengubahan field pun bekerja dengan gaya yang sama seperti struct biasa, padahal nilai fisiknya tinggal di heap.
  6. Baris 20 (show_report(data);): kepemilikan Box berpindah ke fungsi. Setelah baris ini, data mati — pemakaian data setelah baris ini akan ditolak kompilator dengan error E0382 (use of moved value), persis seperti memindahkan String.
  7. Baris 7 (println!("Laporan #{}: ...", data.code, data.summary);): show_report membaca field dengan auto-deref. Saat fungsi selesai, parameter data keluar scope, Box di-drop, dan seluruh petak heap — struct beserta teks summary — dibebaskan.

Kapan Memakai Box #

Pedoman praktis pemakaian Box<T>:

  • Tipe rekursif seperti linked list atau pohon yang berisi dirinya sendiri — Box memutus perhitungan ukuran tak hingga (Chapter berikutnya).
  • Data besar yang sering berpindah antar fungsi lewat move — memindahkan pointer 8 byte lebih murah daripada menyalin struct raksasa.
  • Trait object Box<dyn Trait> ketika tipe konkrit perlu disembunyikan — sudah kita praktikkan di Section Polimorfisme.
  • Nilai kembalian besar dari fungsi, agar stack pemanggil tidak menumpuk salinan sementara.

Ringkasan #

Box<T> mengalokasikan satu nilai di heap dan menempatkan pointer 8 byte di stack sebagai pemiliknya: alokasi terjadi di Box::new, pembacaan dan penulisan isi berjalan lewat operator * atau auto-deref yang transparan, dan pembersihan heap terjadi otomatis saat Box keluar scope. Box tidak mengubah aturan ownership sedikit pun — kepemilikan tetap tunggal dan perpindahannya tetap tunduk pada move semantics. Keterbatasan “satu pemilik” ini menyisakan satu pertanyaan besar: apa yang terjadi jika sebuah tipe benar-benar harus berisi dirinya sendiri, padahal ukurannya harus bisa dihitung saat kompilasi? Jawabannya adalah penggunaan Box yang paling klasik, dan kita buktikan lewat error E0072 pada Chapter berikutnya.