পাঠ ১৫.৪

Rc<T>: reference counted smart pointer

Rc<T>, the Reference Counted Smart Pointer

সাধারণত ownership স্পষ্ট — কোন variable কোন value-এর owner সেটা জানা থাকে। কিন্তু কিছু ক্ষেত্রে একটা value-এর একাধিক owner দরকার হয়। যেমন graph data structure-এ — একটা node-এ একাধিক edge point করতে পারে; conceptually সবাই সেই node-এর owner। Node-টা drop হবে তখনই, যখন কেউ আর reference করছে না।

এই multiple ownership-কে explicit ভাবে enable করতে — Rust-এর Rc<T> type। Name এসেছে reference counting থেকে — value-এর reference কয়টা active সেটা track করে। Reference zero হলে value cleanup, কোনো reference invalid হয় না।

কখন Rc<T> দরকার

ভাবো — family room-এ একটা TV। প্রথম যে ঢুকবে সে on করবে; পরে আরো লোক ঢুকতে পারে; সর্বশেষ যে room থেকে বের হবে সে off করবে। মাঝখানে কেউ off করে দিলে বাকিদের চিৎকার শুরু হবে। Rc-ও এমন।

আমরা Rc<T> use করি — যখন heap-এ কিছু data multiple part-এ read করবে, এবং আগে থেকে compile time-এ বলা যায় না কোন part সবচেয়ে শেষে শেষ করবে। জানলেই সেটাকে owner বানাতাম — normal ownership rule।

Note: Rc<T> only single-threaded। Multi-thread-এ reference counting আলাদা type — চ্যাপ্টার ১৬-তে।

Box দিয়ে sharing — ভেঙে যায়

আগের পাঠ-এর cons list — দেখা যাক একই list দু'জনে share করার চেষ্টা Box দিয়ে।

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

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

fn main() {
    let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}
compile errortext
error[E0382]: use of moved value: `a`
  --> src/main.rs:11:30
   |
 9 |     let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
    |         - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 |     let b = Cons(3, Box::new(a));
    |                              - value moved here
11 |     let c = Cons(4, Box::new(a));
    |                              ^ value used here after move

Cons variant তার data own করে। b বানাতে গিয়ে a move হয়ে গেল b-তে। c-তে আর a available নেই। Reference দিয়েও করা যেত — কিন্তু lifetime parameter দিতে হত, এবং বলতে হত list-এর element list-এর চেয়ে বেশিদিন বাঁচে। সব scenario-তে এটা সত্যি না।

Rc দিয়ে fix

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

use crate::List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, Rc::clone(&a));
    let c = Cons(4, Rc::clone(&a));
}

Rc prelude-এ নেই — use std::rc::Rc; দরকার। প্রথম Rc-তে a বানালাম। b ও c বানানোর সময় Rc::clone(&a) — যেটা reference count বাড়ায়, ১ থেকে ২, তারপর ২ থেকে ৩।

a.clone()-ও কাজ করত, কিন্তু convention হলো Rc::clone(&a)। কারণ — Rc::clone deep copy করে না, শুধু count বাড়ায়; বেশিরভাগ type-এর clone deep copy করে (expensive)। দু'জনের জন্য একই syntax থাকলে performance-এর জন্য code review করতে গিয়ে কোনটা cheap কোনটা costly বোঝা কঠিন। তাই Rust community deep clone vs ref-count clone visually আলাদা রাখার জন্য Rc::clone use করে।

Reference count দেখা

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

use crate::List::{Cons, Nil};
use std::rc::Rc;

// --snip--

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("count after creating a = {}", Rc::strong_count(&a));
    let b = Cons(3, Rc::clone(&a));
    println!("count after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, Rc::clone(&a));
        println!("count after creating c = {}", Rc::strong_count(&a));
    }
    println!("count after c goes out of scope = {}", Rc::strong_count(&a));
}

Rc::strong_count দিয়ে count পড়া যায়।strong_count নাম এজন্য — Rc-তে আবার একটা weak_count-ও আছে (পরের পাঠ — reference cycle)।

Output:

count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2

a-এর initial count ১। প্রতিটা clone ১ বাড়ায়; c scope-এর বাইরে গেলে ১ কমে। Decrement-এর জন্য আমরা কোনো function call করিনি — Rc-এর Drop implementation automatic count কমায়।

Main শেষে b তারপর a scope-এর বাইরে যায় — count ০ হয়, Rc সম্পূর্ণ cleanup। Multiple owner থাকলেও Rc নিশ্চিত করে — যতদিন একজনও owner আছে value valid।

Limitation — শুধু read

Rc<T> immutable reference দিয়ে data share করতে দেয় — শুধু read। যদি multiple mutable reference allow করত, borrowing rule ভাঙত — data race, inconsistency। কিন্তু কখনো মুটেট-ও করতে চাই! সেটার সমাধান পরের পাঠে — RefCell<T> এবং interior mutability।

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

  • Rc<T> — multiple ownership, reference count tracking; count ০ হলে cleanup।
  • Rc::clone(&a) count বাড়ায়, deep copy না; performance-এ cheap।
  • Rc::strong_count(&a) দিয়ে count পড়া যায়।
  • Drop impl automatic count decrement করে — আলাদা function call লাগে না।
  • Single-threaded only; mutable share-এর জন্য RefCell + Rc।