Debug School

rakesh kumar
rakesh kumar

Posted on

How to Store data on the Heap in rust

Achieve Runtime Polymorphism (dyn Trait)
Runtime Polymorphism (Interface vs Trait Object)
Difference between Box vs Rc vs Arc

Store data on the Heap (instead of Stack)

Why?

Stack memory is small and fixed-size
Enter fullscreen mode Exit fullscreen mode
Heap allows large or dynamically-sized data
Enter fullscreen mode Exit fullscreen mode

Example

fn main() {
    let x = Box::new(10);

    println!("{}", x);
}

Enter fullscreen mode Exit fullscreen mode

What happens?

10 is stored on the heap

x (the pointer) is on the stack

✅ Use when data is large or you want heap allocation.

2️⃣ Enable Recursive Data Structures (MOST COMMON USE)

Rust does not allow infinite-size types on the stack.

❌ This is NOT allowed
enum List {
Cons(i32, List), // ❌ infinite size
Nil,
}

✅ Correct way using Box

enum List {
    Cons(i32, Box<List>),
    Nil,
}

fn main() {
    let list = List::Cons(1, Box::new(
        List::Cons(2, Box::new(
            List::Nil
        ))
    ));
}
Enter fullscreen mode Exit fullscreen mode

Why Box?

Box has a known size

Enables linked lists, trees, graphs

✅ This is the #1 reason Box exists

3️⃣ Achieve Runtime Polymorphism (dyn Trait)

Traits don’t have a fixed size.

❌ This is NOT allowed
let a: dyn Animal; // ❌ size unknown

✅ Correct with Box

trait Animal {
    fn sound(&self);
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn sound(&self) {
        println!("Bark");
    }
}

impl Animal for Cat {
    fn sound(&self) {
        println!("Meow");
    }
}

fn main() {
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog),
        Box::new(Cat),
    ];

    for a in animals {
        a.sound();
    }
}
Enter fullscreen mode Exit fullscreen mode

Output

Bark
Meow
Enter fullscreen mode Exit fullscreen mode

✅ Use Box for runtime polymorphism
(similar to interfaces + new in Java)

4️⃣ Reduce Stack Size (Performance & Safety)

Large structs can overflow stack.

Example

struct LargeData {
    data: [u8; 1_000_000],
}

fn main() {
    let big = Box::new(LargeData {
        data: [0; 1_000_000],
    });

    println!("Allocated safely on heap");
}
Enter fullscreen mode Exit fullscreen mode

✅ Prevents stack overflow
✅ Safer for big objects

5️⃣ Transfer Ownership Without Copying Large Data
Example

fn take_ownership(x: Box<i32>) {
    println!("{}", x);
}

fn main() {
    let a = Box::new(42);
    take_ownership(a); // ownership moved
}
Enter fullscreen mode Exit fullscreen mode

Why useful?

Only pointer moves (cheap)

Heap data stays in one place

6️⃣ Box Enables Self-Referential / Tree-like Structures
Example: Binary Tree

enum Tree {
    Node(i32, Box<Tree>, Box<Tree>),
    Empty,
}

fn main() {
    let tree = Tree::Node(
        10,
        Box::new(Tree::Empty),
        Box::new(Tree::Empty),
    );
}
Enter fullscreen mode Exit fullscreen mode

✅ Used in:

ASTs

Compilers

Expression trees

7️⃣ Box vs Reference (&T)

Big Idea (One line)

Rust Box is closest to Java’s new object allocation on the heap — but with strict ownership rules.

Java:

Dog d = new Dog();
Enter fullscreen mode Exit fullscreen mode

Rust:

let d = Box::new(Dog);
Enter fullscreen mode Exit fullscreen mode

Both:

Allocate object on heap

Variable holds a reference/pointer

Used for dynamic size / polymorphism

Heap Allocation (Java default vs Rust explicit)
Java (default behavior)

class Person {
    int age;
}

public class Main {
    public static void main(String[] args) {
        Person p = new Person(); // heap
        System.out.println(p.age);
    }
}
Enter fullscreen mode Exit fullscreen mode

Objects are always on heap

Developer doesn’t think about memory location

Rust (explicit with Box)

struct Person {
    age: i32,
}

fn main() {
    let p = Box::new(Person { age: 30 });
    println!("{}", p.age);
}
Enter fullscreen mode Exit fullscreen mode

Recursive Data Structures (IMP difference)
Java – works naturally
class Node {
int value;
Node next; // allowed
}

Why?

Java references have fixed size

Rust – ❌ without Box (NOT allowed)

enum List {
    Cons(i32, List), // ❌ infinite size
    Nil,
}

Rust – ✅ with Box
enum List {
    Cons(i32, Box<List>),
    Nil,
}

Enter fullscreen mode Exit fullscreen mode

Why Java doesn’t need Box?

Java reference size is fixed

Rust needs compile-time size

📌 This is the #1 place where Box has NO Java equivalent

3️⃣ Runtime Polymorphism (Interface vs Trait Object)
Java (Interface)

interface Animal {
    void sound();
}

class Dog implements Animal {
    public void sound() { System.out.println("Bark"); }
}

class Cat implements Animal {
    public void sound() { System.out.println("Meow"); }
}

public class Main {
    public static void main(String[] args) {
        Animal a1 = new Dog();
        Animal a2 = new Cat();

        a1.sound();
        a2.sound();
    }
}

Enter fullscreen mode Exit fullscreen mode

Rust (Trait Object needs Box)

trait Animal {
    fn sound(&self);
}

struct Dog;
struct Cat;

impl Animal for Dog {
    fn sound(&self) { println!("Bark"); }
}

impl Animal for Cat {
    fn sound(&self) { println!("Meow"); }
}

fn main() {
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog),
        Box::new(Cat),
    ];

    for a in animals {
        a.sound();
    }
}
Enter fullscreen mode Exit fullscreen mode

Comparison

Passing Objects to Functions
Java

void process(Person p) {
    System.out.println(p.age);
}

process(new Person());
Enter fullscreen mode Exit fullscreen mode

Reference copied

Object stays on heap

Rust

fn process(p: Box<Person>) {
    println!("{}", p.age);
}

fn main() {
    let p = Box::new(Person { age: 20 });
    process(p); // ownership moved
}
Enter fullscreen mode Exit fullscreen mode

Prevent Stack Overflow (Large Objects)
Java

class Big {
    byte[] data = new byte[1_000_000];
}
Enter fullscreen mode Exit fullscreen mode

Heap allocation by default

Rust

struct Big {
    data: [u8; 1_000_000],
}
Enter fullscreen mode Exit fullscreen mode
fn main() {
    let b = Box::new(Big { data: [0; 1_000_000] });
}
Enter fullscreen mode Exit fullscreen mode

👉 Box in Rust = Java’s default object allocation

difference between Box vs Rc vs Arc

Box – Single Owner (Default Choice)
When to use

One owner only

Recursive structures

Heap allocation

Fastest option

Example

fn main() {
    let a = Box::new(10);
    let b = a;        // ownership moved
    // println!("{}", a); ❌ compile error

    println!("{}", b);
}
Enter fullscreen mode Exit fullscreen mode

Key rule

Only one variable owns the value.

Java analogy
Object a = new Object();
Object b = a; // shared reference (Rust does NOT allow this)

Rust forbids accidental sharing → memory safety.

3️⃣ Rc – Shared Ownership (Single-Threaded)
When to use

Multiple owners

Same thread only

Trees, graphs

UI state, ASTs

Example

use std::rc::Rc;

fn main() {
    let a = Rc::new(10);

    let b = Rc::clone(&a);
    let c = Rc::clone(&a);

    println!("a = {}", a);
    println!("b = {}", b);
    println!("c = {}", c);
    println!("count = {}", Rc::strong_count(&a));
}
Enter fullscreen mode Exit fullscreen mode

Output

a = 10
b = 10
c = 10
count = 3
Enter fullscreen mode Exit fullscreen mode

Key rule

Data is dropped only when count becomes zero.

Java analogy

Object o = new Object();
Object a = o;
Object b = o;
// GC cleans when unreachable

❗ Rc is NOT thread-safe
// ❌ compile error
std::thread::spawn(move || {
    println!("{}", a);
});
Enter fullscreen mode Exit fullscreen mode

4️⃣ Arc – Shared Ownership (Multi-Threaded)
When to use

Multiple owners

Across threads

Concurrent programs

Example

use std::sync::Arc;
use std::thread;

fn main() {
    let a = Arc::new(10);

    let handles: Vec<_> = (0..3).map(|_| {
        let a = Arc::clone(&a);
        thread::spawn(move || {
            println!("{}", a);
        })
    }).collect();

    for h in handles {
        h.join().unwrap();
    }

    println!("count = {}", Arc::strong_count(&a));
}
Enter fullscreen mode Exit fullscreen mode

Output

10
10
10
count = 1
Enter fullscreen mode Exit fullscreen mode

Why Arc?

Uses atomic reference counting

Safe across threads

Java analogy
AtomicReference ref = new AtomicReference<>(10);

5️⃣ Mutability with Rc & Arc (VERY IMPORTANT)
❌ Rc cannot be mutated directly
let x = Rc::new(5);
// x += 1 ❌

Rc + RefCell (Single Thread)

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let x = Rc::new(RefCell::new(5));

    *x.borrow_mut() += 1;
    println!("{}", x.borrow());
}
Enter fullscreen mode Exit fullscreen mode

Arc + Mutex (Multi Thread)

use std::sync::{Arc, Mutex};
use std::thread;

fn main() {
    let x = Arc::new(Mutex::new(5));

    let handles: Vec<_> = (0..3).map(|_| {
        let x = Arc::clone(&x);
        thread::spawn(move || {
            let mut val = x.lock().unwrap();
            *val += 1;
        })
    }).collect();

    for h in handles {
        h.join().unwrap();
    }

    println!("{}", *x.lock().unwrap());
}
Enter fullscreen mode Exit fullscreen mode

6️⃣ Recursive Structure Comparison
Box (single owner)

enum List {
    Cons(i32, Box<List>),
    Nil,
}
Enter fullscreen mode Exit fullscreen mode

Rc (shared tree)

use std::rc::Rc;

enum Node {
    Value(i32),
    Next(Rc<Node>),
}

Arc (shared across threads)
use std::sync::Arc;

enum Node {
    Value(i32),
    Next(Arc<Node>),
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)