পাঠ ১৫.১

Box<T> দিয়ে heap-এ data রাখা

Using Box<T> to Point to Data on the Heap

সবচেয়ে সরল smart pointer হলো box — type হিসেবে Box<T>। Box তোমার data-কে stack-এর বদলে heap-এ রাখে; stack-এ থাকে শুধু সেই heap-data-এর pointer। আগের ৪ নম্বর অধ্যায়ে stack ও heap নিয়ে কথা বলেছিলাম — সেটা মাথায় রাখো।

Box-এর কোনো performance overhead নেই (heap-এ data থাকা ছাড়া), আবার extra capability-ও নেই। তিনটি situation-এ এটা সবচেয়ে কাজে আসে:

  • যখন type-এর size compile time-এ জানা যায় না, কিন্তু এমন context-এ value দরকার যেখানে exact size লাগে।
  • যখন বড় amount data-এর ownership transfer করতে চাও, কিন্তু সেই transfer-এ data copy হোক চাও না।
  • যখন তুমি একটা value own করতে চাও, কিন্তু আগ্রহ শুধু এতেই — সেটা একটা specific trait implement করে কিনা, type কোনটা সেটা না (এটাকে বলে trait object; অধ্যায় ১৮-এ আসবে)।

Heap-এ data store

Syntax আগে দেখি — কীভাবে box-এর সাথে interact হয়। নিচে একটা i32 heap-এ রাখা হচ্ছে:

src/main.rsrust
fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}

b একটা Box — heap-এ থাকা 5-কে point করছে। Print হবে b = 5। যেকোনো owned value-এর মতো, scope-এর শেষে box deallocate হয় — box নিজে (stack-এ) এবং সে যেই data-কে point করছিল (heap-এ), দুটোই।

একটা single value box-এ রাখা সাধারণত খুব useful না — i32-এর মতো জিনিস stack-এ থাকাই ভালো। আসল কাজে box লাগে এমন সব জায়গায় যেখানে box ছাড়া type-ই define করা যায় না।

Recursive type — box ছাড়া অসম্ভব

Recursive type মানে — একটা type-এর value-এর ভেতরে আবার সেই same type-এর value থাকতে পারে। সমস্যা হলো — Rust-কে compile time-এ জানতে হয় একটা type কত space নেবে। কিন্তু recursive nesting theoretically infinite হতে পারে — Rust জানে না কত space লাগবে। Box-এর size তো জানা (একটা pointer-এর size); তাই recursive definition-এ box ঢুকিয়ে দিলে compiler-এর সমস্যা মিটে যায়।

Cons list — example

Recursive type-এর classic example হিসেবে cons list দেখব। এটা Lisp-এর data structure — দুই-element pair-এর nested chain, Lisp-এর version-এর linked list। নাম এসেছে cons function থেকে (construct function), যেটা দু'টা argument থেকে নতুন pair বানায়। একটা pair-এ একটা value আর আরেকটা pair — recursive ভাবে chain করলে পুরো list।

Pseudocode-এ 1, 2, 3 list:

(1, (2, (3, Nil)))

প্রতিটা item-এ দুটো জিনিস — current value, এবং next item। শেষ item-এ value নেই, শুধু Nil — recursion-এর base case। (এটা চ্যাপ্টার ৬-এর "null" বা "nil" না — সেটা invalid value, এটা list-এর ending marker।)

Rust-এ cons list প্রায়শই use হয় না — সাধারণত Vec<T> ভালো। কিন্তু concept-এর জন্য এটা সরল।

প্রথম attempt — compile হবে না

src/main.rsrust
enum List {
    Cons(i32, List),
    Nil,
}

fn main() {}

Note: generics দিয়ে যেকোনো type-এর জন্য বানানো যেত — সরলতার জন্য শুধু i32

List 1, 2, 3 use করতে চাইলে:

src/main.rsrust
enum List {
    Cons(i32, List),
    Nil,
}

// --snip--

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Cons(2, Cons(3, Nil)));
}

Compile করতে গেলে error:

compile errortext
error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
2 |     Cons(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to break the cycle
  |
2 |     Cons(i32, Box<List>),
  |               ++++    +

"infinite size" — কারণ Cons variant-এ আরেকটা List directly। কেন এটা সমস্যা সেটা বোঝার জন্য আগে দেখি Rust non-recursive type-এর size কিভাবে calculate করে।

Non-recursive type-এর size

চ্যাপ্টার ৬-এর Message enum:

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

fn main() {}

Rust প্রতিটা variant দেখে — কোনটা সবচেয়ে বেশি জায়গা নেয়। Quit কিছুই না, Move দুটো i32, ইত্যাদি। যেহেতু একসাথে শুধু একটাই variant হবে — সবচেয়ে বড় variant-এর সমান space-ই যথেষ্ট।

কিন্তু List-এর ক্ষেত্রে — Cons variant-এ একটা i32 + একটা List। সেই List-এর size জানতে আবার Cons-এ ঢুকতে হবে — i32 + List → i32 + List → ... অনন্ত। Compiler-এর কাছে কোনো end নেই।

Box দিয়ে fix

Compiler নিজেই hint দিয়েছে — "Box, Rc, বা & দিয়ে indirection ঢোকাও"। Indirection মানে — value সরাসরি না রেখে value-এর pointer রাখা।

Box<T> একটা pointer — তার size fixed, T যতই বড় হোক না কেন। তাই Cons-এ List-এর জায়গায় Box<List> দিলে — actual List variant-এর ভেতরে না থেকে heap-এ থাকবে, Cons-এ থাকবে শুধু pointer।

src/main.rsrust
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

এখন Cons = i32 + box-এর pointer-data — fixed size। Nil তো কিছুই না। Compile হবে।

Box শুধু indirection আর heap allocation দেয় — অন্য কোনো special capability না। Cons list-এ এটাই দরকার।

Smart pointer হিসেবে box

Box<T>-কে smart pointer বলা হয় কারণ এটা Deref trait implement করে — এর ফলে regular reference-এর মতো behave করে। আবার scope-এর শেষে box drop হলে এর Drop implementation heap-data-ও cleanup করে। পরের দু'টা পাঠে এই দুই trait — যেটা এই অধ্যায়ের বাকি smart pointer-গুলোর foundation।

এই পাঠ থেকে যা শিখলে

  • Box<T> data-কে heap-এ রাখে; stack-এ থাকে pointer।
  • Box-এর তিন main use — recursive/unknown-size type, বড় data move-এ copy এড়ানো, এবং trait object।
  • Recursive type যেমন cons list — direct nesting-এ "infinite size" error; Box<List> দিয়ে indirection ঢুকিয়ে fix।
  • Box smart pointer — Deref এবং Drop implement করে। পরের পাঠে detail।