Moving beyond single-ownership
Up to this point, we have worked exclusively with a single-ownership memory model: Dynamic memory on the heap is owned by a single instance of a type (e.g. Vec<T>
or Box<T>
). Single ownership is often enough to solve many problems in a good way, and it is fairly easy to reason about. You might have heard of the single responsibility principle, which states that every module/class/function should have just one thing that it is responsible forWhich does not mean that it has to do just one thing. Sometimes to get some reasonable amount work done you have to do multiple things, but those things might still be part of the same overall functionality, so you might consider them a single responsibility. For most programming rules, trying to take them too literaly often defeats the purpose of the rule.. The single ownership memory models is somewhat related to that, as having just one owner of a piece of memory makes it clear whose responsibility this piece of memory is.
There are some exceptions to this rule though, where it is useful or even necessary to have multiple owners of the same piece of memory. At a higher level, shared memory would be such a concept, where two or more processes have access to the same virtual memory page(s), using this memory to communicate with each other. In this case of course we have the operating system to oversee the ownership of the memory, but we can find similar examples for dynamic memory allocations. It is however hard to isolate these examples from the broader context in which they operate, so the following examples might seem significantly more complex at first than the previous examples.
An example of multiple ownership
Consider an application that translates text. The user inputs some text in a text field and a translation process is started. Once the translation has finished, it is displayed in a second text field. To keep the application code clean, it is separated into the UI part and the translation part, maybe in two different classes or Rust modules UI
and Translator
. It is the job of the UI
to react to user input and pass the text that should be translated to the Translator
. A translation might take some time, so in order to keep the application responsive, the UI
does not want to wait on the Translator
. Instead, it frequently will check if the Translator
has already finished the translation. As an added feature, the user can cancel a translation request, for example by deleting the text from the text field or overwriting it with some other text that shall be translated instead.
This is an example of an asynchronous communications channel: Two components communicate information with each other without either waiting on the other. In our example, text is communicated between UI
and Translator
, with the UI
sending text in the source language and the Translator
eventually sending text in the target language back (or never sending anything back if the request was cancelled).
A first try at implementing multiple ownership in Rust
Let's try to implement something like the UI
/Translator
system with our new-found Rust knowledge:
struct OngoingTranslation { source_text: String, translated_text: String, is_ready: bool, } impl OngoingTranslation { pub fn new(source_text: String) -> Self { Self {source_text, translated_text: Default::default(), is_ready: false} } } struct UI { outstanding_translations: Vec<Box<OngoingTranslation>>, } impl UI { pub fn new() -> Self { Self { outstanding_translations: vec![], } } pub fn update(&mut self, translator: &mut Translator) { todo!() } fn should_cancel_translation() -> bool { todo!() } } struct Translator {} impl Translator { pub fn update(&mut self) { todo!() } pub fn request_translation(&mut self, text: String) -> Box<OngoingTranslation> { todo!() } } fn main() { let mut translator = Translator{}; let mut ui = UI::new(); loop { ui.update(&mut translator); translator.update(); } }
Here we define our UI
and Translator
types, and the piece of work that represents an ongoing translation as OngoingTranslation
. To simulate a responsive application, both the UI
and the Translator
specify update()
methods which are called in a loop from main
. The UI
can request a new translation from the Translator
, which returns a new OngoingTranslation
instance that encapsulates the translation request. Since this request might live for a while, we put it onto the heap using Box<T>
. Let's look closer at OngoingTranslation
:
#![allow(unused)] fn main() { struct OngoingTranslation { source_text: String, translated_text: String, is_ready: bool, } }
It stores the source_text
as a String
, which is the text that has to be translated. translated_text
is another String
which will eventually contain the translated text. Lastly we have the aforementioned is_ready
flag. An implementation of the UI
update method might look like this:
#![allow(unused)] fn main() { pub fn update(&mut self, translator: &mut Translator) { // Process all translations that are done by printing them to the standard output for translation in &self.outstanding_translations { if translation.is_ready { println!("{} -> {}", translation.source_text, translation.translated_text); } } // Drop everything that has been translated or that should be cancelled. Don't bother with the syntax // for now, think of it as removing all elements in the vector for which 'is_ready' is true self.outstanding_translations = self.outstanding_translations.drain(..).filter(|translation| { !translation.is_ready && !Self::should_cancel_translation() }).collect(); // Create new translation requests self.outstanding_translations.push(translator.request_translation("text".into())); } }
Besides some alien syntax for handling removing elements from the vector, it is pretty straightforward. We use some magic should_cancel_translation()
method to check if a translation should be cancelled, it doesn't matter how this method is implemented. Perhaps most interesting is the last line, where we use the Translator
to create a new translation request. Let's try to write this method:
#![allow(unused)] fn main() { struct Translator { ongoing_translations: Vec<Box<OngoingTranslation>>, } impl Translator { pub fn request_translation(&mut self, text: String) -> Box<OngoingTranslation> { let translation_request = Box::new(OngoingTranslation { source_text: text, translated_text: "".into(), is_ready: false }); self.ongoing_translations.push(translation_request); translation_request } } }
First, we have to change the Translator
type so that it actually stores the OngoingTranslation
s as well. We use Box<OngoingTranslation>
here again, because we want these instances to live on the heap so that we can share them between the Translator
and the UI
. Then in request_translation()
, we create a new OngoingTranslation
instance, push it into self.ongoing_translations
and return it from the method. Let's build this:
error[E0382]: use of moved value: `translation_request`
--> src/chap3_4.rs:69:9
|
63 | let translation_request = Box::new(OngoingTranslation {
| ------------------- move occurs because `translation_request` has type `Box<OngoingTranslation>`, which does not implement the `Copy` trait
...
68 | self.ongoing_translations.push(translation_request);
| ------------------- value moved here
69 | translation_request
| ^^^^^^^^^^^^^^^^^^^ value used here after move
Ah of course, the push()
method takes something by value, so it is moved and then we can't use something after move. We could try to clone()
the translation_request
, however recall that in Rust, cloning a Box
is equal to performing a deep copy. If we did that, than the translation_request
instance in self.ongoing_translations
and the one we return from the request_translation()
method are two different instances! This way, we could never communicate to the UI
that the request is done!
Shared mutable references to the rescue?
So maybe the Translator
does not have to own the OngoingTranslation
s? Let's try storing a reference instead:
#![allow(unused)] fn main() { struct Translator<'a> { ongoing_translations: Vec<&'a mut OngoingTranslation>, } impl <'a> Translator<'a> { pub fn request_translation(&'a mut self, text: String) -> Box<OngoingTranslation> { let translation_request = Box::new(OngoingTranslation { source_text: text, translated_text: "".into(), is_ready: false }); self.ongoing_translations.push(translation_request.as_mut()); translation_request } } }
Since we now store references, we have to specify a lifetime for these references. In request_translation()
, we tie this lifetime 'a
to the lifetime of the &mut self
reference, which effectively says: 'This lifetime is equal to the lifetime of the instance that request_translation()
was called with.' Which seems fair, the OngoingTranslation
s probably don't live longer than the Translator
itself.
You know the drill: Compile, aaaand...
error[E0623]: lifetime mismatch
--> src/chap3_4.rs:39:55
|
25 | pub fn update(&mut self, translator: &mut Translator) {
| ---------------
| |
| these two types are declared with different lifetimes...
...
39 | self.outstanding_translations.push(translator.request_translation("text".into()));
| ^^^^^^^^^^^^^^^^^^^ ...but data from `translator` flows into `translator` here
What is going on here? This error message is a lot harder to read than the previous error messages that we got. Clearly, some lifetimes are different to what the Rust borrow checker is expecting, but it is not clear where exactly. The error message says that this statement right here: &mut Translator
has two types that have different lifetimes. One is the mutable borrow, which has an anonymous lifetime assigned to it, and the second type is the Translator
type itself. Recall that we changed the signature from Translator
to Translator<'a>
at the type definition. Rust has some special rules for lifetime elision, which means that sometimes it is valid to not specify lifetimes explicitly, if the compiler can figure out reasonable default parameters. This is what happened here: We didn't specify the lifetime of the Translator
type, and the compiler chose an appropriate lifetime for us. It just wasn't the right lifetime! Recall that we specified that the request_translation()
method is valid for the lifetime called 'a
of the instance that it is called on. This instance is the mutable borrow translator: &mut Translator
in the function signature of update()
. Without explicitly stating that this is the same lifetime as the one of the Translator
type, the compiler is free to choose two different lifetimes, for example like this:
#![allow(unused)] fn main() { pub fn update<'a, 'b>(&mut self, translator: &'a mut Translator<'b>) { /*...*/ } }
Inside request_translation()
, we then try to convert a mutable borrow that is valid for lifetime 'a
to one that is valid for lifetime 'b
, but 'a
and 'b
are unrelated, so the borrow checker complains. We can fix this by using the same lifetime in both places:
#![allow(unused)] fn main() { pub fn update<'a>(&mut self, translator: &'a mut Translator<'a>) { /*...*/ } }
Now we get a lot of new errors :( Let's look at just one error:
error[E0597]: `translation_request` does not live long enough
--> src/chap3_4.rs:69:40
|
52 | impl <'a> Translator<'a> {
| -- lifetime `'a` defined here
...
69 | self.ongoing_translations.push(translation_request.as_mut());
| -------------------------------^^^^^^^^^^^^^^^^^^^----------
| | |
| | borrowed value does not live long enough
| argument requires that `translation_request` is borrowed for `'a`
70 | translation_request
71 | }
| - `translation_request` dropped here while still borrowed
This happens inside the request_translation()
method at the following line:
#![allow(unused)] fn main() { self.ongoing_translations.push(translation_request.as_mut()); }
Here, we get a mutable borrow of the new translation_request
using the as_mut()
method of the Box<T>
type. We then try to push this mutable borrow into self.ongoing_translations
, which expects &'a mut OngoingTranslation
as type, so a mutable borrow that lives as long as the lifetime 'a
. If we look at the signature of the as_mut()
method, we see that it comes from a trait implementation like this:
#![allow(unused)] fn main() { pub trait AsMut<T: ?Sized> { fn as_mut(&mut self) -> &mut T; } }
At first this might look weird: A function returning a borrow without explicit lifetime specifiers. This is where the lifetime elision rules kick in again. In situations where you have a function that takes a borrow and returns a borrow, the Rust compiler assumes that the two borrows share a lifetime, so an alternate signature of this function would be:
#![allow(unused)] fn main() { fn as_mut<'a>(&'a mut self) -> &'a mut T; }
So in this line right here:
#![allow(unused)] fn main() { self.ongoing_translations.push(translation_request.as_mut()); }
We get a mutable borrow of the contents of translation_request
that lives as long as the variable translation_request
. Which is a local variable inside the request_translation()
function, so of course it does not live long enough to match the lifetime of the Translator
type.
What can we do here? At this point, instead of trying more and more convoluted things, we will directly go to the heart of the problem: We tried to use Box<T>
, a single ownership container, to model a multiple ownership situation. Think about it: Both UI
and Translator
are full owners of an OngoingTranslation
instance. Either the Translator
finishes first, in which case it is the job of the UI
to clean up the memory, or the UI
aborts an ongoing request, which might still be in use by the Translator
, requiring the Translator
to clean up the memory. This situation is unresolvable at compile-time! So we need a type that effectively models this multiple ownership situation. Enter Rc<T>
!