DEV Community

Cover image for Ownership in Rust
Francesco Ciulla
Francesco Ciulla

Posted on

Ownership in Rust

Ownership in Rust

Rust has a concept of ownership that is unique among programming languages. It is a key feature of the language that allows it to be both safe and fast. In this lesson, we will explore what ownership is, how it works, and why it is important.

If you prefer a video version

All the code is available on GitHub (link available in the video description)

In this lesson

In this lesson, we will cover the following topics:

  • What is ownership?
  • The Stack and the Heap
  • Ownership rules
  • The String type
  • Memory allocation and the Drop function
  • The Move trait
  • The Clone trait
  • The Copy trait
  • Final Recap

What is Ownership?

Ownership in Rust - Rust programming tutorial

Ownership is a distinct feature of Rust, enabling safe and efficient memory management.

In Rust, each value has a sole owner responsible for its disposal when it's not needed, eliminating the need for garbage collection or reference counting.

This lesson will be about ownership through examples centered on Strings, a common data structure.

⚠️ Before we proceed, it's important to understand that Ownership is not something that runs in the background or adds an overhead to the program. It's a concept that is enforced at compile time, and it's a key feature of the language that allows it to be both safe and fast.

The Stack and the Heap

Understanding the stack and heap is essential in Rust due to its unique memory management through ownership.

The Stack

The Stack and the Heap

The Stack, used for fixed-size data, provides quick access.

It has some methods to manage the data, like push and pop, and it's very fast.

All the data stored on the stack must have a known, fixed size at compile time, making it ideal for small, fixed-size data.

Data with an unknown size or a size that might change at runtime is stored on the heap, which is slower than the stack due to its dynamic nature.

The Heap

The Heap, on the other hand, is used for data whose size might change or cannot be determined until runtime.

The Stack and the Heap

The Heap is less organized than the stack, and it's slower to access data stored on it.

The momory allocated on the heap is not managed by the Rust compiler, but by the programmer. This is why Rust has a strict ownership model for managing the memory safely and efficiently, preventing memory leaks, and ensuring data is cleaned up properly.

Note: the pointer to the data is stored on the stack, but the data itself is stored on the heap.

Operations on the Stack and the Heap

Here is a recap of the operations on the stack and the heap:

  • The stack is fast and efficient, but it can only store data with a known, fixed size at compile time.

  • The heap is slower and less organized, but it can store data with an unknown size or a size that might change at runtime.

  • The memory allocated on the heap is not managed by the Rust compiler but by the programmer.

  • Functions like push and pop are available for the stack, but not for the heap.

  • The pointer to the data is stored on the stack, but the data itself is stored on the heap.

The Stack and the Heap

Ownership purpose and Rules

Ownership's primary purpose is to manage the data stored on the Heap, ensuring it's cleaned up properly and preventing memory leaks.

To achieve this:

  • it keeps track of what code is using what data on the heap
  • it minimizes the amount of duplicate data on the heap
  • it cleans up the data on the heap when it's no longer needed

Rust has a few ownership rules:

  1. Each value in Rust has a variable that's its owner.
  2. There can only be one owner at a time.
  3. When the owner goes out of scope, the value will be dropped.

Let's see an example.

Variable scope

In Rust, a variable is only valid within the scope it was declared.

Even when a variable is stored on the Stack, it is only valid within the scope it was declared.

fn main() {
{                      // s is not valid here, it’s not yet declared
        let s = "hello";   // s is valid from this point forward

        // do stuff with s
    }                      // this scope is now over, and s is no longer valid
}
Enter fullscreen mode Exit fullscreen mode

In the example above, s is the owner of the string "hello". When s goes out of scope, the string will not be valid anymore.

The String type

In a previous lesson, we covered simple, fixed-size data types in Rust.

Now, we'll explore the String type, a compound type allocated on the heap, with a detailed look planned for a future lesson.

We've touched on string literals, which are immutable and embedded in the program. Unlike these, the String type is mutable and heap-allocated, allowing it to hold text of variable length, unknown at compile time.

let s = String::from("hello");
Enter fullscreen mode Exit fullscreen mode

This kind of string can be mutated:

    let mut s = String::from("hello");

    s.push_str(", world!"); // push_str() appends a literal to a String

    println!("{}", s); // This will print `hello, world!`
Enter fullscreen mode Exit fullscreen mode

So, what’s the difference here?

The key difference lies in their memory handling: String can be mutated due to its dynamic memory allocation on the heap, while literals, stored in fixed memory, cannot be changed.

Memory Allocation and the Drop Function

    {
        let s = String::from("hello"); // s is valid from this point forward

        // do stuff with s
    } // this scope is now over, and s is no longer valid
Enter fullscreen mode Exit fullscreen mode

When a variable goes out of scope, Rust calls a special function for us.

This function is called drop, and it’s where the author of String can put the code to return the memory.

Rust calls drop automatically at the closing curly bracket.

The Move Trait

If we try to do something like that:

    let s1 = 5;
    let s2 = s1;

    // print s2
    println!("{}, ", s2);

    // print s1
    println!("{}, ", s1);
Enter fullscreen mode Exit fullscreen mode

This code is valid and it will print 5 twice.

If we do something like that:

    let s1 = "hello";
    let s2 = s1;

    // print s2
    println!("{}, ", s2);

    // print s1
    println!("{}, ", s1);
Enter fullscreen mode Exit fullscreen mode

This code is also valid, and it will print hello twice.

But if we type something like that:

    let s1 = String::from("hello");
    let s2 = s1;

    // print s2
    println!("{}, ", s2);

    // print s1
    println!("{}, ", s1);
Enter fullscreen mode Exit fullscreen mode

This code will throw a compile-time error!

Why? Because Rust considers s1 to be invalid after the assignment to s2. This is because Rust has a special trait called Move that is implemented for the String type.

Below is a schema of what happens. We might think that when we use the = operator, we copy the pointer or the whole data again, but this is not what happens: We MOVE the pointer to the new variable (that's why it's called Move trait).

The Move Trait

The s1 variable is no longer valid after the assignment to s2.

To 'fix' our code, we can use the clone method (as suggested by the compiler):

    let s1 = String::from("hello");
    let s2 = s1.clone();

    // print s2
    println!("{}, ", s2);

    // print s1
    println!("{}, ", s1);
Enter fullscreen mode Exit fullscreen mode

The Clone Trait

If we call the clone method, what happens is exactly what is shown in the schema below: we copy the data and the pointer to the new variable.

The Clone Trait

So why do we need the clone? Because for the variables stored on the heap, Rust, by default, does not copy the data when we assign a variable to another. It only copies the pointer to the data. This is because copying the data would be expensive in terms of performance.

So if we use the clone method or we read someone else using the clone method in their code, we know that this was intentional and that the author of the code wanted to copy the data and the pointer to the new variable.

Stack-Only Data: Copy

So now you might be wondering: "Hey, if we need to use the clone method to copy the data and the pointer to the new variable, why didn't we need to use the clone method when we copied the integer or the string literal?"

    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);
Enter fullscreen mode Exit fullscreen mode

The Copy Trait

Integers, being simple fixed-size values, are stored directly on the stack, enabling Rust to copy their bits from one variable to another without invalidation upon scope exit. This is because there's no distinction between deep and shallow copying for such types, making bit-copy safe.

Rust utilizes the Copy trait for types like integers that reside on the stack, ensuring automatic data duplication without runtime overhead. However, types with the Drop trait, which require special cleanup, cannot implement Copy. Adding Copy to such types would lead to compile-time errors, as it contradicts the need for controlled resource release.

Types eligible for the Copy trait include simple scalar values that don't need dynamic allocation or aren't complex resources.

Examples include

  • all integer types (e.g., u32)
  • the Boolean type (bool)
  • floating-point types (e.g., f64)
  • the character type (char)
  • tuples containing only Copy types (e.g., (i32, i32)).

Conversely, tuples with non-Copy components, like (i32, String), do not implement Copy.

Final Recap

In this lesson, we covered the following topics:

  • What is ownership?
  • The Stack and the Heap
  • Ownership rules
  • The String type
  • Memory allocation and the Drop function
  • The Move trait
  • The Clone trait
  • The Copy trait

In the next lesson, we will cover references and borrowing in Rust.

If you prefer a video version

All the code is available on GitHub (link available in the video description)

You can find me here: Francesco

Top comments (5)

Collapse
 
rdrpenguin04 profile image
Ray Redondo

There is a lot wrong with this as written.

  • Stack is not a type in Rust. The stack is an abstract place, but describing it as a type that can be used is unhelpful.
  • Likewise with Heap; calling it a type is misleading.
  • Copy objects can be put on the stack or the heap. For example, an array of 1024 u16 values can technically be put on the stack, but if you get too many of those, it'll overflow, so it's wiser to use the heap.
  • Move is not a trait! Moving is what happens when Copy is not implemented, but moving doesn't have a trait corresponding to it. What would such an implementation even look like? Or, what would happen if Move and Copy were implemented?
Collapse
 
francescoxx profile image
Francesco Ciulla

Hi Ray, and thanks for addressing these things. here is my answers to all of them:

  • Stack is definitively not a type, I couldn't find where I mention that Stack is a type in the article. Can you please point it out?
  • Same for the heap
  • you are correct, but this is an introductory article, and it's meant for beginners to understand the difference between the stack and the heap. I didn't mention Box, Vec in this article as it's a more advanced one.
  • you are right about calling "Move" a trait; it's more like a feature of the language
Collapse
 
rdrpenguin04 profile image
Ray Redondo

The main thing I noticed is that Stack and Heap are given styling as if they're types. The first thing I noticed was actually this quote:

It has some methods to manage the data, like push and pop, and it's very fast.

In reality, the stack is abstract and does not have methods. For someone who only knows Rust, saying it has methods is probably a bit confusing, but as someone who was taught Java first (which actually does have a Stack type) and as someone who knows Rust's VecDeque (something that has push and pop as I recall), the way the abstract stack was described seemed extremely close to describing an actual type with no clarification that it isn't.

Collapse
 
get_pieces profile image
Pieces 🌟

I learn more about Rust with each article, thank you. 😊

Collapse
 
francescoxx profile image
Francesco Ciulla

you are welcome, @get_pieces

Some comments may only be visible to logged-in visitors. Sign in to view all comments.