Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Cover image for Smart Pointers in Rust: What, why and how?
Roger Torres (he/him/ele)
Roger Torres (he/him/ele)

Posted on

     

Smart Pointers in Rust: What, why and how?

TL;DR: I will cover some of Rust's smart pointers:Box,Cell,RefCell,Rc,Arc,RwLock andMutex.

It seems pretty obvious that smart pointers are pointers that are...smart. Butwhat exactly does this "smart" means?When should we use them?How do they work?

These are the questions I will begin to answer here. And that is it:the beginning of an answer, nothing more. I expect that this article will give you a "background of understanding" (something like "familiarity" with the concept) that will help you accommodate a real understanding of the topic, which will come from reading the official documentation and, of course, practice.

If you are already familiar with it, you may use this text as a list of relevant reading. Look for the "useful links" at the beginning of each section.

Index:

  1. Box
  2. Cell
  3. RefCell
  4. Rc
  5. Arc
  6. RwLock
  7. Mutex

Smart points in general

As explained inThe Book, pointers are variables containing an address that "points at" some other data. The usual pointer in Rust is the reference (&). Smart pointers are pointers that "have additional metadata and capabilities", e.g., they may count how many times the value was borrowed, provide methods to manage read and write locks, etc.

Technically speaking,String andVec are also smart pointers, but I will not cover them here as they are quite common and are usually thought of as types rather than pointers.

Also note that, from this list, onlyArc,RwLock, andMutex are thread-safe.


Box<T>

Useful links:The Book;Documentation;Box, stack and heap.

What?

Box<T> allows you to storeT on the heap. So, if you have, say, au64 that would be stored on the stack,Box<u64> will store it on the heap.

If you are not comfortable with the concepts of stack and heap, readthis.

Why?

Values stored on the stack cannot grow, as Rust needs to know its size at compile time. The best example of how this may affect your programming that I know is inThe Book:recursion. Consider the code below (and its comments).

// This does not compile. The List contains itself,// being recursive and therefore having an infinite size.enumList{Cons(i32,List),Nil,}// This does compile because the size of a pointer// does not change according to the size of the pointed value.enumList{Cons(i32,Box<List>),Nil,}
Enter fullscreen modeExit fullscreen mode

Be sure to readthis section onThe Book's to understand the details.

On a more general note,Box is useful when your value is too big to be kept on the stack or when you need toown it.

How?

To get the valueT insideBox<T> you just have to deref it.

letboxed=Box::new(11);assert_eq!(*boxed,11)
Enter fullscreen modeExit fullscreen mode

Cell<T>

Useful links:Module documentation;Pointer documentation.

What?

Cell<T> gives a shared reference toT while allowing you to changeT. This is one of the "shareable mutable containers" provided by the modulestd::cell.

Why?

In Rust, shared references are immutable. This guarantees that when you access the inner value you are not getting something different than expected, as well as assure that you are not trying to access the value after it was freed (which is a big chunk of those70% of security bugs that are memory safety issues).

How?

WhatCell<T> does is provide functions that control our access toT. You can find them allhere, but for our explanation we just need two:get() andset().

Basically,Cell<T> allows you to freely changeT withT.set() because when you useT.get(), you retrieveCopy ofT, not a reference. That way, even if you changeT, the copied value you got withget() will remain the same, and if you destroyT, no pointer will dangle.

One last note is thatT has to implementCopy as well.

usestd::cell::Cell;letcontainer=Cell::new(11);leteleven=container.get();{container.set(12);}lettwelve=container.get();assert_eq!(eleven,11);assert_eq!(twelve,12);
Enter fullscreen modeExit fullscreen mode

RefCell<T>

Useful links:The Book;Documentation.

What?

RefCell<T> also gives shared reference toT, but whileCell is statically checked (Rust checks it at compile time),RefCell<T> is dynamically checked (Rust checks it at run time).

Why?

BecauseCell operates with copies, you should restrict yourself to using small values with it, which means that you need references once again, which leads us back to the problem thatCell solved.

The wayRefCell deals with it is by keeping track of who is reading and who writingT. That's whyRefCell<T> is dynamically checked: becauseyou are going to code this check. But fear not, Rust will still make sure you don't mess up at compile time.

How?

RefCell<T> has methods that borrow either a mutable or immutable reference toT; methods that will not allow you to do so if this action would be unsafe. As withCell, there are several methods inRefCell, but these two are enough to illustrate the concept:borrow(), which gets an immutable reference; andborrow_mut(), which gets a mutable reference. The logic used byRefCell goes something like this:

  • If there isno reference (either mutable or immutable) toT, you may get either a mutable or immutable reference to it;
  • If there is already amutable reference toT, you may get nothing and got to wait until this reference is dropped;
  • If there are one or moreimmutable references toT, you may get an immutable reference to it.

As you can see, there is no way to get both mutable and immutable references toT at the same time.

Remember: this isnot thread-safe. When I say "no way", I am talking about a single thread.

Another way to think about is:

  • Immutable references areshared references;
  • Mutable references areexclusive references.

It is worth to say that the functions mentioned above have variants that do not panic, but returnsResult instead:try_borrow() andtry_borrow_mut();

usestd::cell::RefCell;letcontainer=RefCell::new(11);{let_c=container.borrow();// You may borrow as immutable as many times as you want,...assert!(container.try_borrow().is_ok());// ...but cannot borrow as mutable because// it is already borrowed as immutable.assert!(container.try_borrow_mut().is_err());}// After the first borrow as mutable...let_c=container.borrow_mut();// ...you cannot borrow in any way.assert!(container.try_borrow().is_err());assert!(container.try_borrow_mut().is_err());
Enter fullscreen modeExit fullscreen mode

Rc<T>

Useful links:The Book;Module documentation;Pointer documentation;Rust by example.

What?

I will quote the documentation on this one:

The typeRc<T> provides shared ownership of a value of typeT, allocated on the heap. Invokingclone onRc produces a new pointer to the same allocation on the heap. When the lastRc pointer to a given allocation is destroyed, the value stored in that allocation (often referred to as “inner value”) is also dropped.

So, like aBox<T>,Rc<T> allocatesT on the heap. The difference is that cloningBox<T> will give you anotherT inside anotherBox while cloningRc<T> gives you anotherRc to thesameT.

Another important remark is that we don't have interior mutability inRc as we had inCell orRefCell.

Why?

You want to have shared access to some value (without making copies of it), but you want to let it go once it is no longer used, i.e., when there is no reference to it.

As there is no interior mutability inRc, it is common to use it alongsideCell orRefCell, for example,Rc<Cell<T>>.

How?

WithRc<T>, you are using theclone() method. Behind the scene, it will count the number of references you have and, when it goes to zero, it dropsT.

usestd::rc::Rc;letmutc=Rc::new(11);{// After borrwing as immutable...let_first=c.clone();// ...you can no longer borrow as mutable,...assert_eq!(Rc::get_mut(&mutc),None);// ...but can still borrow as immutable.let_second=c.clone();// Here we have 3 pointer ("c", "_first" and "_second").assert_eq!(Rc::strong_count(&c),3);}// After we drop the last two, we remain only with "c" itself.assert_eq!(Rc::strong_count(&c),1);// And now we can borrow it as mutable.letz=Rc::get_mut(&mutc).unwrap();*z+=1;assert_eq!(*c,12);
Enter fullscreen modeExit fullscreen mode

Arc<T>

Useful links:Documentation;Rust by example.

What?

Arc is the thread-safe version ofRc, as its counter is managed through atomic operations.

Why?

I think the reason why you would useArc instead ofRc is clear (thread-safety), so the pertinent question becomes: why not just useArcevery time? The answer is that these extra controls provided byArc come with an overhead cost.

How?

Just likeRc, withArc<T> you will be usingclone() to get a pointer to the same valueT, which will be destroyed once the last pointer is dropped.

usestd::sync::Arc;usestd::thread;letval=Arc::new(0);foriin0..10{letval=Arc::clone(&val);// You could not do this with "Rc"thread::spawn(move||{println!("Value: {:?} / Active pointers: {}",*val+i,Arc::strong_count(&val));});}
Enter fullscreen modeExit fullscreen mode

RwLock<T>

Useful link:Documentation.

RwLock is also provided by theparking_lot crate.

What?

As a reader-writer lock,RwLock<T> will only give access toT once you are holding one of the locks:read orwrite, which are given following these rules:

  • To read: If you want a lock to read, you may get it as long asno writer is holding the lock; otherwise, you have to wait until it is dropped;
  • To write: If you want a lock to write, you may get as long asno one, reader or writer, is holding the lock; otherwise, you have to wait until they are dropped;

Why?

RwLock allows you to read and write the same data from multiple threads. Different fromMutex (see below), it distinguishes the kind of lock, so you may have severalread locks as far as you do not have anywrite lock.

How?

When you want to read aRwLock, you got to use the functionread()—ortry_read()—that will return aLockResult that contains aRwLockReadGuard. If it is successful, you will be able to access the value insideRwLockReadGuard by using deref. If a writer is holding the lock, the thread will be blocked until it can take hold of the lock.

Something similar happens when you try to usewrite()—ortry_write(). The difference is that it will not only wait for a writer holding the lock, but for any reader holding the lock as well.

usestd::sync::RwLock;letlock=RwLock::new(11);{let_r1=lock.read().unwrap();// You may pile as many read locks as you want.assert!(lock.try_read().is_ok());// But you cannot write.assert!(lock.try_write().is_err());// Note that if you use "write()" instead of "try_write()"// it will wait until all the other locks are released// (in this case, never).}// If you grab the write lock, you may easily change itletmutl=lock.write().unwrap();*l+=1;assert_eq!(*l,12);
Enter fullscreen modeExit fullscreen mode

If some thread holding the lock panics, further attempts to get the lock will return aPoisonError, which means that from then on every attempt to readRwLock will return the samePoisonError. You may recover from a poisonedRwLock usinginto_inner().

usestd::sync::{Arc,RwLock};usestd::thread;letlock=Arc::new(RwLock::new(11));letc_lock=Arc::clone(&lock);let_=thread::spawn(move||{let_lock=c_lock.write().unwrap();panic!();// the lock gets poisoned}).join();letread=matchlock.read(){Ok(l)=>*l,Err(poisoned)=>{letr=poisoned.into_inner();*r+1}};// It will be 12 because it was recovered from the poisoned lockassert_eq!(read,12);
Enter fullscreen modeExit fullscreen mode

Mutex<T>

Useful links:The Book;Documentation.

Mutex is also provided by theparking_lot crate.

What?

Mutex is similar toRwLock, but it only allows one lock-holder, no matter if it is a reader or a writer.

Why?

One reason to preferMutex overRwLock is thatRwLock may lead to writer starvation (when the readers pile on and the writer never gets a chance to take the lock, waiting forever), something the does not happen withMutex.

Of course, we are diving into deeper seas here, so the real-life choice falls on more advanced considerations, such as how many readers you expect at the same time, how the operating system implements the locks, and so on...

How?

Mutex andRwLock work in a similar fashion, the difference is that, becauseMutex does not differentiate between readers and writers, you just uselock() ortry_lock to get theMutexGuard. The poisoning logic also happens here.

usestd::sync::Mutex;letguard=Mutex::new(11);letmutlock=guard.lock().unwrap();// It does not matter if you are locking the Mutex to read or write,// you can only lock it once.assert!(guard.try_lock().is_err());// You may change it just like you did with RwLock*lock+=1;assert_eq!(*lock,12);
Enter fullscreen modeExit fullscreen mode

You can deal with poisonedMutex in the same way as you deal with poisonedRwLock.


Thank you for reading!

Top comments(4)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss
CollapseExpand
 
fgadaleta profile image
frag
🏢 Founder of Amethix 🌟 Building software wizardry and 🦀 Rust-powered wonders 🎧 Host of the mind-bending podcast datascienceathome.com
  • Location
    Belgium
  • Education
    PhD. Computer Science
  • Work
    Chief Software Engineer at Amethix
  • Joined

Very good explanation. Thanks for sharing!

CollapseExpand
 
acelot profile image
acelot
  • Joined

Fantastic article! Thank you, man. Finally someone explained things to me in simple terms )

CollapseExpand
 
rogertorres profile image
Roger Torres (he/him/ele)
A dev who writes technical texts in ordinary language.
  • Joined

I'm really glad to hear that,@acelot!

CollapseExpand
 
nfrankel profile image
Nicolas Fränkel
Dev Advocate | Developer & architect | Love learning and passing on what I learned!
  • Location
    Geneva
  • Work
    Developer Advocate
  • Joined

I'm learning Rust. Those are very good pointers!

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

A dev who writes technical texts in ordinary language.
  • Joined

More fromRoger Torres (he/him/ele)

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp