Previous Next

The Accelerated Guide to Smart Pointers in Rust (Tim McNamara) (z-library.sk, 1lib.sk, z-lib.sk)

Author: Tim McNamara

RUST

A short textbook for anyone who's interested in understanding about Rust's smart pointers, including people who don't know what smart pointers are.

📄 File Format: PDF
💾 File Size: 603.8 KB
6
Views
0
Downloads
0.00
Total Donations

📄 Text Preview (First 20 pages)

ℹ️

Registered users can read the full content for free

Register as a Gaohf Library member to read the complete e-book online for free and enjoy a better reading experience.

📄 Page 1
SMART POINTERS IN RUST TIM McNAMARA The Accelerated Guide to
📄 Page 2
accelerant.dev First published by Accelerant Press, Lower Hutt, New Zealand. © Tim McNamara 2023 ISBN: 978-0-473-67972-9 (paperback) ISBN: 978-0-473-67973-6 (EPUB) ISBN: 978-0-473-67974-3 (Kindle) ISBN: 978-0-473-67975-0 (PDF) A catalogue record for this book is available from the National Library of New Zealand. Kei te pātengi raraunga o Te Puna Mātauranga o Aotearoa te whakarārangi o tēnei pukapuka.
📄 Page 3
The Accelerated Guide to Smart Pointers in Rust Tim McNamara A short textbook for anyone who’s interested in understanding about Rust’s smart pointers, including people who don’t know what smart pointers are.
📄 Page 4
Introduction In this guide, we embark on a journey to explore the various types of smart pointers available in Rust, their use cases, and how they contribute to managing memory safely and efficiently. In some sense, they are the essence of Rust’s “zero-cost abstraction” philosophy, whereby you only pay for what you use. Smart pointers are powerful tools that provide additional functionality and guarantees compared to raw pointers. If you’re unsure what a raw pointer is, that’s okay, we’ll discuss that too. We’ll cover the core smart pointer types in Rust, including Box<T> , Rc<T> , Arc<T> , RefCell<T> , and Mutex<T> . Each type will be thoroughly explained, accompanied by practical code examples that are linked directly in the Rust playground, so you can run them in your web browser. Furthermore, we will delve into best practices and common pitfalls associated with using smart pointers in Rust. This knowledge will empower you to write robust, efficient, and maintainable code while avoiding potential pitfalls along the way. Whether you are a Rust enthusiast, a curious learner, or a seasoned developer looking to enhance your memory management skills, this guide is here to support your journey. So let’s dive in and unlock the power of smart pointers in Rust! We begin by learning about what the term “smart pointer” actually means. 3
📄 Page 5
Contents 2. Defining smart pointers .................................................................................... 6 3. Understanding Rust ............................................................................................ 8 3.1. Ownership ...................................................................................................... 10 3.2. Borrowing ....................................................................................................... 12 3.3. Lifetimes ........................................................................................................ 14 4. Defining smart pointers, again .................................................................... 19 5. Why use them? .................................................................................................... 21 5.1. Automatic memory management ...................................................... 22 5.2. Prevent data races ................................................................................... 23 5.3. Add super powers to pointers ............................................................ 25 5.4. Simplify code .............................................................................................. 26 6. Stdlib’s smart pointers .................................................................................... 27 6.1. Box<T> ........................................................................................................... 28 6.2. Rc<T> ............................................................................................................. 32 6.3. Arc<T> ........................................................................................................... 36 6.4. RefCell<T> ................................................................................................... 39 6.5. Mutex<T> ..................................................................................................... 42 6.6. RwLock<T> .................................................................................................. 45 7. Building your own smart pointers .............................................................. 47 7.1. Drop .................................................................................................................. 48 7.2. Deref ................................................................................................................ 54 7.3. DerefMut ....................................................................................................... 58 8. Extension topics ................................................................................................ 62 8.1. Cyclic data structures ............................................................................. 63 8.2. Rc<T> from scratch ................................................................................. 64 8.3. PhantomData<T> ...................................................................................... 67 4
📄 Page 6
9. Recap ...................................................................................................................... 70 10. Cheat Sheet ........................................................................................................ 71 11. Afterword .............................................................................................................. 73 12. About Tim McNamara .................................................................................... 74 5
📄 Page 7
Defining smart pointers A smart pointer is a data structure that not only points to an object in memory but also provides additional features, such as dynamically allocating memory as required or reference counting. By encapsulating these responsibilities, smart pointers can help ensure memory safety and reduce the likelihood of programming errors, such as memory leaks or dangling pointers. In the Rust programming language, smart pointers are an essential tool for safe and efficient memory management while adhering to Rust’s ownership rules. Even if you’re unfamiliar with the term, you have already encountered a smart pointer. The String and its cousin Vec<T> are both smart pointers. As well as holding references to the backing array that’s storing the data, they both provide contain the current length and capacity of that array. Now let’s connect the abstract concept of a smart pointer to some Rust-specific concepts. First, a smart pointer owns the data that it refers to. Some raw pointers, such as Rc<T> offer shared ownership, but that shouldn’t be conflated with the reality that the initial pointer needs to have ownership over whatever it is counting references about. Secondly, smart pointers typically implement Deref<Target = U> where U is the data type that’s being pointed at. This means – along with Rust’s auto-deref behavior – that it’s possible for application programmers to use a smart pointer type in place of its referent largely without fuss. 6
📄 Page 8
You may have also heard the term fat pointer being used as a synonym for smart pointer, but for the purposes of this document we’ll consider these two terms to be distinct. They’re often used interchangeably because a fat pointer includes some metadata about the referent along side the memory address. That metadata is typically the length. This provides some extra capabilities over a raw pointer – specifically it’s possible to deduce what a valid memory access would be without needing to interpret any bytes. [Sidenote: This contrasts with text strings in C, which require the application to check whether the next byte is NULL ( 0x0 ) whenever it accesses the data.] However, because &T and &mut T are not considered smart pointers in the Rust ecosystem, and because they contain a length field when referring to dynamically-sized types (DSTs), we’ll avoid the use of the term fat pointer. 7
📄 Page 9
Understanding Rust Rust is a modern, programming language designed with performance, reliability, and productivity in mind. It originated in the world of systems programming and provides low-level control over system resources, similar to languages like C and C++, while offering strong guarantees for memory safety and thread safety, like managed languages such as Java and Python. Its expressive type system is much closer to something like Haskell than C. Its no-fuss build system has enabled a rich ecosystem of 3rd party packages. Memory safety is a crucial aspect of Rust. The language eliminates common programming errors at compile time, such as null pointer dereferences, buffer overflows, and many—but not all—data races, which can lead to security vulnerabilities and hard-to-debug issues. Rust achieves this by introducing a unique ownership system, coupled with borrowing and lifetime rules, which are checked at compile-time, ensuring that your code is safe without incurring runtime overhead. Smart pointers are a key tool in Rust’s memory management toolkit. They are data structures that not only hold a reference to an object in memory but also provide additional features, such as automatic memory management or reference counting. By using smart pointers, developers can productively work within Rust’s strict ownership and borrowing rules. They are considered a zero cost 8
📄 Page 10
abstraction because they are a compile-time construct that the compiler “boils away” during the build process. As we begin our journey, it’s essential first to grasp the concepts of ownership, borrowing and lifetimes. If you’ve skipped these concepts so far, please do take the time to read through the next few sections because gaining an understanding of what is happening will be very beneficial to you want to undesrstand how some of the smart pointer types behave. 9
📄 Page 11
Ownership Ownership is a core concept in Rust that allows it to guarantee memory safety without a garbage collector. The ownership system revolves around three primary rules: 1. Each value in Rust has a variable that’s called 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 look at some code to illustrate these rules in action: fn main() {     let s1 = String::from("hello");     let s2 = s1;     println!("{s1}"); } We first create a String value and bind it to the variable s1 . Then, we bind the variable s2 to the value of s1 . Lastly, we try to print the value of s1 . If you try to compile this code, you’ll get an error because Rust’s ownership rules prevent it. When we assigned the value of s1 to s2 , Rust moved the ownership of the underlying String value from s1 to s2 . As a result, accessing s1 is no longer valid, and trying to use it will result in a compile-time error. This behavior prevents any potential double-free bugs or invalid memory access, ensuring memory safety. Now let’s look at another example where ownership is transferred through a function call: 10
📄 Page 12
fn main() {     let s1 = String::from("hello");     takes_ownership(s1);     println!("{s1}"); } fn takes_ownership(text: String) {     println!("I have taken ownership of: {text}"); } Here, we first create a String value and bind it to the variable s1 . We then call the takes_ownership() function, passing s1 as an argument. The ownership of the String value is moved from s1 to the text parameter in the function. As before, trying to print the value of s1 after transferring ownership produces a compile-time error. 11
📄 Page 13
Borrowing Rust provides a mechanism to temporarily “borrow” ownership of a value, allowing it to be used without transferring ownership permanently. There are two types of borrowing in Rust: immutable (also known as a shared borrow) and mutable (also known as a unique borrow). Let’s first see how immutable borrowing works: fn main() {     let s1 = String::from("hello");     let only_ascii_bytes = only_ascii_bytes(&s1);     if only_ascii_bytes {         println!("Thank goodness for Unicode!");     }     println!("{s1}"); } fn only_ascii_bytes(s: &String) ‑> bool {     s.is_ascii() } In this example, we pass a reference to s1 to the only_ascii_bytes() function using the & symbol, known as the reference operator. This creates an immutable borrow of s1 , which means the function can use the value without taking ownership. This allows us to print the value of s1 after the function call without any issues. Mutable borrowing is similar but allows the borrowed value to be modified. Here’s an example: 12
📄 Page 14
fn main() {     let mut s1 = String::from("hello");     append_world(&mut s1);     println!("{s1}"); } fn append_world(s: &mut String) {     s.push_str(" world"); } In this example, we first create a mutable String value – notice the mut keyword – and bind it to the variable s1 . We then call the append_world() function, passing a mutable reference to s1 using the &mut operator. This creates a mutable borrow of s1 , which allows the function to modify the value without taking ownership. Inside the append_world() function, we use the push_str() method String to append “ world” – string literals are of type “string slice”, &str , rather than String – to the original String . After the function call, we can print the modified value of s1 without triggering a compiler error about a moved value. It’s important to note that Rust enforces certain restrictions when it comes to borrowing: 1. Multiple immutable borrows can coexist: This is why they are also known as shared borrows. 2. Either one or the other, but not both, type of borrow can exist at the same time: You can have one or more immutable borrows, or one mutable borrow, but not both. This is sometimes referred to as the XOR rule. 13
📄 Page 15
Lifetimes Now that we have a solid understanding of ownership and borrowing, let’s dive into the concept of lifetimes and their annotations in Rust. Lifetimes are used to express the scope of a reference and ensure that references are valid for the duration of their use. Rust’s borrow checker uses lifetimes to prevent dangling references, which occur when a reference outlives the data it refers to. By default, Rust can infer lifetimes in most cases, so you don’t need to explicitly annotate them. However, there are situations where you need to provide lifetime annotations to help the compiler understand how references relate to each other. Let’s look at a small code example to understand the need for lifetime annotations: fn main() {     let s1 = String::from("hello");     let s2 = String::from("world");     let result = find_longest(&s1, &s2);     println!("{result}"); } fn find_longest(t1: &str, t2: &str) ‑> &str {     use std::cmp::Ordering::*;     match t1.len().cmp(&t2.len()) {         Greater => &t1,         Equal => "",         Less => &t2,     } } 14
📄 Page 16
In this example, we have a function, find_longest() that takes references to two String values and returns a reference to the longest one. The fragment t1.len().cmp(&t2.len()) compares the lengths of t1 and t2 , returning a std::cmp::Ordering , which is then matched against. If you try to compile this code, you’ll get an error because Rust cannot determine whether the lifetime of the returned reference, which is itself a borrow, should be tied to s1 and s2 . To fix this, can add lifetime annotations to indicate that s1 and s2 have the same lifetime. Here’s the modified code with lifetime annotations: fn main() {     let s1 = String::from("hello");     let s2 = String::from("world");     let result = find_longest(&s1, &s2);     println!("{result}"); } fn find_longest<'a>(t1: &'a str, t2: &'a str) ‑> &'a str {     use std::cmp::Ordering::*;     match t1.len().cmp(&t2.len()) {         Greater => &t1,         Equal => "",         Less => &t2,     } } In this updated code, we’ve added a lifetime parameter 'a to the find_longest() function. This parameter is used to annotate the input references and the return type. The lifetime annotation 'a tells Rust that the returned reference will live at least as long as the shortest of the input references. You may have noticed that the empty string literal doesn’t play a large role in this process. String literals have the special 'static lifetime that indicates their lifetime will remain for the rest of the program. 15
📄 Page 17
Adding lifetime annotations does not change the lifetimes of the references. They are a way to express the relationships between the lifetimes of different references, helping the compiler verify that your code doesn’t create any dangling references. Let’s look at another example to understand how lifetimes work, this time using structs: struct Person<'a> {     name: &'a str, } impl<'a> Person<'a> {     fn new(name: &'a str) ‑> Person<'a> {         Person { name }     }     fn greet(&self) {         println!("Hello, my name is {}.", self.name);     } } fn main() {     let name = String::from("Alice");     let person = Person::new(&name);     person.greet(); } In this example, we have a Person struct with a lifetime parameter 'a . The struct contains a name field, which is a reference to a string with the same lifetime as the struct. The new method and the greet() method in the impl block also use the same lifetime parameter to ensure that the Person instance and the name field have compatible lifetimes. In the main() function, we create a String value name and then create a Person instance using the new() static method. Since the lifetime of the name variable is the same as the Person instance, the code compiles and runs successfully, printing the greeting as intended. Let’s create a slightly more complicated example to try to bring everything together. We’ll create a simple program that 16
📄 Page 18
demonstrates ownership, borrowing, and lifetimes through a Book and Author types in a library. struct Author<'a> {     name: &'a str, } struct Book<'a> {     title: &'a str,     author: Author<'a>,     publication_year: i32, } impl<'a> Author<'a> {     fn new(name: &'a str) ‑> Author<'a> {         Author { name }     } } impl<'a> Book<'a> {     fn new(         title: &'a str,         author: Author<'a>,         publication_year: i32     ) ‑> Book<'a> {         Book {             title,             author,             publication_year,         }     }     fn display(&self) {         println!(             "{} ({}) by {}",             self.title, self.publication_year, self.author.name         );     } } fn main() {     let author_name = "Maya Angelou";     let author = Author::new(&author_name);     let book_title = "I Know Why the Caged Bird Sings"; 17
📄 Page 19
    let book = Book::new(&book_title, author, 1969);     book.display();     let author_name2 = "Chimamanda Ngozi Adichie";     let author2 = Author::new(&author_name2);     let book_title2 = "Americanah";     let book2 = Book::new(&book_title2, author2, 2013);     book2.display(); } [playground] In our library example, we define two structs, Author and Book , both with a lifetime parameter 'a . Re-using lifetime parameter names, particularly 'a , is common and does not imply that that the lifetimes are necessarily shared between the contexts using that name. Think of it being similar to a variable, but for lifetimes. The new() methods for both the Author and Book structs both make use of a lifetime parameter to ensure that the instances and their respective fields have compatible lifetimes. In the Book struct’s impl block, we also define a display() method to print out the book’s information. The display() method borrows the Book instance immutably, which is itself borrowing an Author immutably, which is also borrowing a String immutably. The original instances remain accessible after the method call. One consideration with borrowing is that adding a borrow places a constraint on an owner. The value’s owner, that is the variable that is bound to the value, is not able to become invalid to access until after the lifetimes of all of the references to the value have ended. 18
📄 Page 20
Defining smart pointers, again So, what are smart pointers? Smart pointers are data structures that act like pointers but have additional features, such as automatic memory management, enabling shared ownership and interior mutability. Unlike raw pointers, smart pointers implement traits that allow them to provide these extra features, making them safer and more convenient to use. To explain why they exist, it might be worthwhile to consider writing Rust without smart pointers. The following example shows how easy it is to avoid Rust’s ownership system. fn main() {     let x = 42;     let ptr = &x as *const _;     drop(x);     let y = unsafe { *ptr };     println!("{y}"); } In this example, we create an immutable reference to the variable x , then immediately cast it as a raw pointer as ptr . The syntax *const _ creates a “const pointer”, one that does not modify what is being referred to and asks Rust to infer the correct type. x is 19
The above is a preview of the first 20 pages. Register to read the complete e-book.

💝 Support Author

0.00
Total Amount (¥)
0
Donation Count

Login to support the author

Login Now

Recommended for You

Loading recommended books...
Failed to load, please try again later
Back to List