Setiap kali Rust mengalokasikan variabel di stack atau memindahkan nilai antar fungsi, kompilator perlu menjawab satu pertanyaan: berapa byte ukuran nilai ini? Sebagian besar tipe yang telah kita pakai menjawab pertanyaan itu dengan angka tetap — i32 selalu 4 byte, &i32 selalu 8 byte pada arsitektur 64-bit. Tipe seperti itu disebut sized: ukurannya diketahui lengkap saat kompilasi. Namun ada keluarga tipe yang tidak bisa menjawab: [i32] bisa berisi 3 elemen atau 3 juta elemen, str bisa sepanjang 5 karakter atau 500, dan dyn Trait bisa menampung implementasi apa pun. Tipe-tipe itu disebut unsized atau DST (dynamically sized type). Section slice dan smart pointer sebelumnya sudah membuat kita akrab memakai tipe ini lewat &[T], &str, dan Box<dyn Trait>; chapter ini membuka mesin di baliknya — fat pointer — dan menjelaskan mengapa unsized type selalu harus bersembunyi di belakang pointer.
Ukuran yang Diketahui Saat Kompilasi #
Kompilator menghitung ukuran setiap tipe secara statis. Fungsi std::mem::size_of::<T>() memaparkan hasil perhitungan itu, dan size_of_val mengukur byte yang benar-benar ditempati sebuah nilai. Mari kita ukur beberapa tipe yang telah lalu:
fn main() {
println!("Ukuran i32 : {} byte", std::mem::size_of::<i32>());
println!("Ukuran &i32 : {} byte", std::mem::size_of::<&i32>());
println!("Ukuran [i32; 6] : {} byte", std::mem::size_of::<[i32; 6]>());
println!("Ukuran &[i32] : {} byte", std::mem::size_of::<&[i32]>());
println!("Ukuran &str : {} byte", std::mem::size_of::<&str>());
println!("Ukuran String : {} byte", std::mem::size_of::<String>());
}Output yang Dihasilkan:
Ukuran i32 : 4 byte
Ukuran &i32 : 8 byte
Ukuran [i32; 6] : 24 byte
Ukuran &[i32] : 16 byte
Ukuran &str : 16 byte
Ukuran String : 24 byteEmpat pengamatan penting dari output di atas:
i32= 4 byte: tipe sized paling sederhana; ukurannya tertanam di sistem tipe.&i32= 8 byte: sebuah reference hanya menyimpan alamat, jadi ukurannya tetap 8 byte pada mesin 64-bit berapa pun ukuran nilai yang ditunjuk.[i32; 6]= 24 byte: array berukuran tetap adalah tipe sized karena jumlah elemennya menjadi bagian dari tipe — 6 elemen dikali 4 byte per elemen.&[i32]dan&strmasing-masing 16 byte: keduanya dua kali ukuran reference biasa. Angka ini adalah petunjuk pertama keberadaan fat pointer yang akan kita bedah sebentar lagi.
Tipe yang Ukurannya Tidak Tetap #
Tipe [i32] — slice tanpa panjang — berbeda dari [i32; 6]: tipe tersebut tidak menyimpan jumlah elemen, sehingga kompilator tidak bisa menghitung ukurannya. Hal yang sama berlaku pada str dan dyn Trait. Karena itulah Rust menempatkan aturan ketat: nilai dengan tipe unsized tidak boleh berdiri sendiri sebagai variabel atau parameter. Percobaan melanggarnya berakhir di kompilasi:
fn main() {
let numbers: [i32] = [1, 2, 3];
println!("Jumlah elemen: {}", numbers.len());
}Pesan error lengkapnya:
error[E0308]: mismatched types
--> main.rs:2:26
|
2 | let numbers: [i32] = [1, 2, 3];
| ----- ^^^^^^^^^ expected `[i32]`, found `[i32; 3]`
| |
| expected due to this
error[E0277]: the size for values of type `[i32]` cannot be known at compilation time
--> main.rs:2:9
|
2 | let numbers: [i32] = [1, 2, 3];
| ^^^^^^^ doesn't have a size known at compile-time
|
= help: the trait `Sized` is not implemented for `[i32]`
= note: all local variables must have a statically known size
help: consider borrowing here
|
2 | let numbers: &[i32] = [1, 2, 3];
| +
error: aborting due to 2 previous errors
Some errors have detailed explanations: E0277, E0308.
For more information about an error, try `rustc --explain E0277`.Dua error tersebut saling melengkapi. E0277 menyebut inti masalahnya: Sized tidak diimplementasikan untuk [i32], dan setiap variabel lokal harus punya ukuran statis. E0308 muncul karena array literal [1, 2, 3] bertipe [i32; 3] yang tidak bisa dijadikan [i32] begitu saja. Perhatikan saran perbaikan dari kompilator: tambahkan & sehingga variabel menjadi &[i32] — reference ke slice. Inilah pintu keluar resminya.
Fat Pointer di Balik Referensi Slice #
Mengapa &[i32] berukuran 16 byte, dua kali &i32? Karena reference ke tipe unsized adalah fat pointer: dua kata data yang berjalan berpasangan. Untuk &[i32] dan &str, pasangan itu terdiri dari alamat byte pertama data dan jumlah elemen (atau jumlah byte untuk str). Mari kita bedah isi fat pointer dengan nyata:
fn main() {
let numbers: [i32; 6] = [10, 20, 30, 40, 50, 60];
let slice: &[i32] = &numbers[1..4];
println!("Panjang slice: {}", slice.len());
println!("Isi slice: {:?}", slice);
println!("Ukuran data slice: {} byte", std::mem::size_of_val(slice));
}Output yang Dihasilkan:
Panjang slice: 3
Isi slice: [20, 30, 40]
Ukuran data slice: 12 byteMari kita bedah kode di atas:
- Baris 2 (
let numbers: [i32; 6] = ...): array pemilik data — tipe sized[i32; 6]sepanjang 24 byte di stack. - Baris 3 (
let slice: &[i32] = &numbers[1..4];): operasi pengindeksan rentang menghasilkan fat pointer 16 byte: satu alamat menuju elemen20di dalam array, plus panjang3. Tidak ada data yang disalin — hanya alamat dan panjang yang dicatat. - Baris 5 (
slice.len()): panjang diambil dari separuh kedua fat pointer, bukan dihitung ulang dari data. Berbeda dariVec,strjuga menyimpan panjang di fat pointer, bukan di dalam data. - Baris 7 (
std::mem::size_of_val(slice)): mengalikan panjang fat pointer dengan ukuran per elemen — 3 elemen dikali 4 byte = 12 byte. Angka ini mengukur data yang ditunjuk, bukan ukuran fat pointer-nya.
Struktur fat pointer dapat digambarkan sebagai berikut:
Diagram di atas menegaskan pembagian kerja: pointer data menunjuk awal jendela, length menentukan lebarnya. Dua slice berbeda dari array yang sama hanya berbeda pada pasangan angka 16 byte ini — alamat dan panjang — tanpa menyentuh data aslinya. Mekanisme yang sama berlaku untuk &str: string literal dan hasil String dipandang lewat fat pointer berisi alamat byte pertama dan panjang byte teks.
Fat Pointer untuk dyn Trait #
Bentuk kedua fat pointer muncul pada dyn Trait. Pada Chapter Trait Object kita memakai &dyn Trait dan Box<dyn Trait> tanpa membahas ukurannya; kini kita bisa membedahnya. Fat pointer dyn Trait tetap berukuran 16 byte, tetapi pasangan kata keduanya berbeda: alamat nilai konkret plus alamat vtable — tabel berisi alamat fungsi-fungsi milik implementasi yang sedang dibungkus. Itulah sebabnya pemanggilan method lewat dyn Trait disebut dynamic dispatch:
trait Shape {
fn area(&self) -> f64;
}
struct Circle {
radius: f64,
}
struct Square {
side: f64,
}
impl Shape for Circle {
fn area(&self) -> f64 {
3.14159 * self.radius * self.radius
}
}
impl Shape for Square {
fn area(&self) -> f64 {
self.side * self.side
}
}
fn main() {
let circle = Circle { radius: 7.0 };
let square = Square { side: 5.0 };
let shapes: [&dyn Shape; 2] = [&circle, &square];
println!("Ukuran &dyn Shape: {} byte", std::mem::size_of::<&dyn Shape>());
for shape in shapes {
println!("Luas: {:.2}", shape.area());
}
}Output yang Dihasilkan:
Ukuran &dyn Shape: 16 byte
Luas: 153.94
Luas: 25.00Mari kita bedah bagian kuncinya:
- Baris 29 (
let shapes: [&dyn Shape; 2] = [&circle, &square];): array berisi dua fat pointer — keduanya bertipe&dyn Shapeyang berukuran 16 byte. Nilaicircledansquaretetap tinggal di stack sebagai tipe konkret; hanya pandangannya yang diubah. - Fat pointer
&circle: kata pertama berisi alamat structCircle, kata kedua berisi alamat vtable khususShape for Circle. Vtable inilah yang menyimpan alamat fungsiareaversiCircle. - Fat pointer
&square: pasangan serupa, tetapi vtable-nya milikShape for Square. - Baris 33-35 (
for shape in shapes { ... }): pemanggilanshape.area()membaca alamat fungsi dari vtable milik masing-masing elemen, lalu melompat ke sana. Baris output “153.94” dihasilkan versiCircle, dan “25.00” dihasilkan versiSquare— perbedaan perilaku dari fat pointer yang berbeda vtable-nya.
Melemaskan Batasan Sized dengan ?Sized #
Terakhir, mari kita buka rahasia di balik aturan “semua harus sized”. Parameter generik secara bawaan menerima batasan implisit T: Sized. Batasannya bisa dilonggarkan dengan penulisan T: ?Sized — dibaca “T boleh sized atau tidak”. Dengan batasannya dilonggarkan, fungsi generik dapat bekerja untuk str, [T], maupun tipe sized biasa, asalkan nilai diakses lewat reference:
fn print_data_size<T: ?Sized>(value: &T) {
println!("{} byte", std::mem::size_of_val(value));
}
fn main() {
print_data_size(&42i32);
print_data_size("Bandung");
let names = [String::from("Budi"), String::from("Siti")];
print_data_size(&names[..]);
}Output yang Dihasilkan:
4 byte
7 byte
48 byteMari kita bedah kode di atas:
- Baris 1 (
fn print_data_size<T: ?Sized>(value: &T) {): batas?Sizedmenghapus tuntutan bahwaTharus punya ukuran compile-time. Nilai datang selalu sebagai&T, dan fat pointer-lah yang mengurus ukuran runtime bilaTunsized. - Baris 6 (
print_data_size(&42i32);):Tmenjadii32— tipe sized;&i32tetap reference biasa 8 byte, dan data terukur 4 byte. - Baris 7 (
print_data_size("Bandung");):Tmenjadistr— tipe unsized; argument menjadi fat pointer&str16 byte, dansize_of_valmembaca panjang 7 byte dari fat pointer tersebut. - Baris 10 (
print_data_size(&names[..]);):Tmenjadi[String]— unsized; hasil&names[..]adalah fat pointer menuju duaStringdi dalam array, sehingga data terukur 48 byte (dua structStringmasing-masing 24 byte).
Satu fungsi dengan delapan baris kini melayani tipe sized dan unsized sekaligus. Signature ?Sized seperti ini banyak dipakai di standard library — misalnya banyak method pada Box menerima T: ?Sized — dan menjadi alat utama saat kita menulis abstraksi sendiri di atas DST.
Ringkasan #
Tipe sized punya ukuran tetap yang dihitung kompilator, dan seluruh parameter generik secara bawaan menuntutnya. Tipe unsized — [T], str, dyn Trait — tidak punya ukuran compile-time, sehingga tidak boleh berdiri sendiri sebagai variabel; pelanggarannya dilaporkan lewat E0277 yang menyarankan peminjaman. Kedua keluarga tipe ini bertemu di fat pointer: &[T] dan &str membawa (pointer, length) berukuran 16 byte, &dyn Trait membawa (pointer data, vtable) berukuran 16 byte, dan batas ?Sized membolehkan fungsi generik menerima keduanya. Setelah mesin pointer selesai dibedah, kita berpindah ke type parameter yang bahkan tidak menyimpan data sama sekali: PhantomData pada Chapter berikutnya.