Ownership
How ownership works in Rust?
Memory in Rust
There are 2 main types of memory in Rust: Stack and Heap.
Stack & Heap
💡 Stack memory is faster to access than heap memory due to its fixed location and automatic management.
Stack is used to store:
Primitive data types (
integers
,floats
,booleans
)Small fixed-size
structs
andarrays
Function arguments (usually)
Local variables within a function
💡 Heap memory is suitable for data that may not be known at compile time or needs to persist beyond a function's lifetime.
Heap is used to store:
Large data structures (e.g., dynamically resizing vectors, binary trees)
Data that needs to live longer than the function that created it (e.g., global variables)
Data allocated with
Box<T>
,Rc<T>
, orArc<T>
Example of How Stack & Heap are stored data:
Static
In Rust, there is a special region of memory to store data is static
which refers to a specific way to declare items with a global lifetime and allocate memory for them.
💡 Static memory is ideal for constant data that won't change throughout the program and requires thread-safe access. They are stored in a special region of memory separate from the stack and heap. Using static
items judiciously help avoid unnecessary memory usage and complexity.
Some of the key characteristics of static are:
Immutable by default: Once initialized, the value of an
static
item cannot be changed.Thread-safe: Multiple threads can access the same
static
item concurrently without data races (assuming proper synchronization if needed).Zero-cost abstraction: Accessing an
static
item is as fast as accessing a normal variable, unlike some heap allocations.
To be able to create a static type, we use a static
keyword.
NAME
: Identifier for your static item.TYPE
: Data type of the static item.VALUE
: Constant expression used to initialize the static item during compilation.
Since static
items are allocated outside the stack and heap, they cannot be dynamically allocated or deallocated during program execution. Therefore, overusing static
items can increase the memory footprint of your program.
If you need to modify the value of an static
item, you can use the lazy_static
crate, which provides mechanisms for lazy initialization and potential mutability.
Both static
and const
are used for constant values. However, const
items are evaluated at compile time and generally limited to primitive data types and simple expressions. On the other hand, the static
items are evaluated at program startup and can hold more complex data types.
static
items
Special memory region
Entire program execution
Immutable by default (can use lazy_static
for mutability)
Global constants, configuration values
const
items
Resolved during compilation
Entire program execution (implicitly)
Immutable
Simple constants, compile-time calculations
When to Use static
static
Defining constants that won't change throughout the program (e.g., mathematical constants, configuration values).
Sharing data across different modules or functions without dynamic allocation overhead.
The Differences Between Stack, Heap & Static
Contents
Primitive data types (integers, floats, booleans), small fixed-size structs and arrays, function arguments, local variables
Large data structures (vectors, binary trees), data that needs to live longer than a function
Constants, global variables
Size
Limited (typically a few megabytes)
Dynamically expandable
Limited (depends on program size)
Lifetime
Function call (data is deallocated when the function exits)
Entire program execution (or until manually deallocated)
Entire program execution
Memory allocation
Automatic by the compiler
Manual using Box<T>
, vec!
, or similar smart pointers
Automatic by the compiler during program startup
Clean up processing
Automatic when the function exits
Manual (memory leak possible if not deallocated properly)
Not required (automatically cleaned up at program termination)
Mutability
Can be mutable or immutable
Can be mutable or immutable
Immutable by default (use lazy_static
for mutability)
Use cases
Local variables, function arguments, temporary data
Dynamically sized data structures, data shared across modules, long-lived objects
Constants, configuration values, global data that needs to be thread-safe
What is Ownership?
💡 Rust's ownership system is closely tied to memory management. Ownership ensures that there's always a single owner responsible for a piece of data, preventing dangling pointers and memory leaks.
Ownership Based Resource Management (OBRM)
💡 OBRM stands for Ownership Based Resource Management. It's a core concept in Rust that governs how memory and other resources are managed within your program.
Benefits of OBRM
Unlike languages with garbage collection, where memory is automatically reclaimed, Rust's OBRM requires you to be explicit about ownership.
This might seem more complex initially, but it gives you fine-grained control over memory usage and avoids the unpredictable behavior of garbage collectors.
Memory safety: OBRM prevents memory leaks and dangling pointers, common issues in languages with manual memory management.
Improved performance: By avoiding unnecessary copying and by ensuring proper deallocation, OBRM contributes to efficient memory usage and program performance.
Code clarity: Ownership rules enforce clear data ownership relationships, making code easier to understand and reason about.
Ownership System
The foundation of OBRM is Rust's ownership system. It ensures that there's always a single owner responsible for a piece of data at any given time.
This ownership is transferred or moved between variables and functions, preventing dangling pointers and memory leaks.
The ownership rules:
Every value in Rust has a single owner. This owner is responsible for the lifetime and deallocation of the value's memory.
Ownership is moved when a value is assigned or passed by value to a function. The original variable loses ownership, and the new variable becomes the owner.
Ownership is dropped (deallocated) when the owner goes out of scope. This happens when a variable goes out of scope or a function returns.
Ownership and Data Types
Primitive data types (integers, floats): Don't have ownership themselves. When passed around, they are copied by value, so ownership isn't a concern for them. When a primitive value is assigned to a variable, the value itself is copied onto the stack. This means, both the original variable and the new variable point to independent copies of the data. The memory on the stack gets automatically deallocated when the function that created the variable exits.
Complex data types (structs, strings): Have ownership. When ownership is moved, the data itself is also moved in memory. When ownership is dropped, the memory is automatically deallocated. Because the complex data types like structs
and strings
can be larger and more expensive to copy. Rust's ownership system ensures efficient memory management by preventing unnecessary copying. It also helps avoid dangling pointers (where a variable points to memory that has already been deallocated).
Examples
Complex Data Passed into Functions (Ownership)
When you call reverse_string(greeting)
, the ownership of the string "Hello, world!"
(stored in greeting
) is moved into the function.
The function creates a new String
(reversed
) and populates it with reversed characters from the original string.
Since ownership of greeting
moved out, trying to access it in main
after the call will lead to a compile error, as it's no longer valid in that scope.
Borrowing with References
If you only need to read the string contents within the function without modifying it, you can use references.
In this case, reverse_string
takes a reference (&str
) to the string, allowing it to borrow the data without taking ownership. This keeps the original string intact in main
.
Variable Passed into Function with Primitive Types (Copy)
The double
function takes an i32
argument x
.
When you call double(num)
, the value of num
(5) is copied into x
within the function. This is because i32
is a primitive type and copying is efficient.
Inside the function, x
is modified, but the original num
in main
remains unchanged and accessible.
Ownership Moves Out of Function with Primitive Types (Return Value)
In this case, the get_sum
takes two i32
arguments and returns their sum.
When you call get_sum(num1, num2)
, the values of num1
and num2
are copied into the function's arguments (x
and y
). The function performs the calculation and returns the sum.
Since it's a copy, the original values of num1
and num2
in main
remain unchanged and accessible.
After the calculation, the ownership of get_num
function is assigned to the sum variable.
Allocation Memory On The Heap
In Rust, we can allocate memory on the heap using the Box
smart pointer.
The Move Semantics
The difference between Rust and C++ is that move semantics are implicit
Shared Ownership
In Rust, shared ownership can be achieved using the Rc
(Reference Counted) and Arc
(Atomic Reference Counted) smart pointers. These types allow multiple ownership of the same data, with Rc
being used for single-threaded scenarios and Arc
for multi-threaded scenarios.
Using Rc<T>
for Shared Ownership in Single-Threaded Contexts
Rc<T>
for Shared Ownership in Single-Threaded Contexts Rc<T>
enables multiple parts of your program to read from the same data without needing to make copies.
Using Arc<T>
for Shared Ownership in Multi-Threaded Contexts
Arc<T>
for Shared Ownership in Multi-Threaded ContextsArc<T>
is similar to Rc<T>
but is thread-safe and can be used in multi-threaded environments.
Last updated