Rc<T>, the Reference Counted Smart Pointer

In the majority of cases, ownership is very clear: you know exactly which variable owns a given value. However, this isn't always the case; sometimes, you may actually need multiple owners. For this, Rust has a type called Rc<T>. Its name is an abbreviation for reference counting. Reference counting means keeping track of the number of references to a value in order to know if a value is still in use or not. If there are zero references to a value, we know we can clean up the value without any references becoming invalid.

To think about this in terms of a real-world scenario, it's like a TV in a family room. When one person comes in the room to watch TV, they turn it on. Others can also come in the room and watch the TV. When the last person leaves the room, they'll turn the TV off since it's no longer being used. If someone turns off the TV while others are still watching it, though, the people watching the TV would get mad!

Rc<T> is for use when we want to allocate some data on the heap for multiple parts of our program to read, and we can't determine at compile time which part of our program using this data will finish using it last. If we knew which part would finish last, we could make that part the owner of the data and the normal ownership rules enforced at compile time would kick in.

Note that Rc<T> is only for use in single-threaded scenarios; the next chapter on concurrency will cover how to do reference counting in multithreaded programs. If you try to use Rc<T> with multiple threads, you'll get a compile-time error.

Using Rc<T> to Share Data

Let's return to our cons list example from Listing 15-5. In Listing 15-11, we're going to try to use List as we defined it using Box<T>. First we'll create one list instance that contains 5 and then 10. Next, we want to create two more lists: one that starts with 3 and continues on to our first list containing 5 and 10, then another list that starts with 4 and also continues on to our first list containing 5 and 10. In other words, we want two lists that both share ownership of the third list, which conceptually will be something like Figure 15-10:

Two lists that share ownership of a third list

Figure 15-10: Two lists, b and c, sharing ownership of a third list, a

Trying to implement this using our definition of List with Box<T> won't work, as shown in Listing 15-11:

Filename: src/main.rs

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let a = Cons(5,
        Box::new(Cons(10,
            Box::new(Nil))));
    let b = Cons(3, Box::new(a));
    let c = Cons(4, Box::new(a));
}

Listing 15-11: Having two lists using Box<T> that try to share ownership of a third list won't work

If we compile this, we get this error:

error[E0382]: use of moved value: `a`
  --> src/main.rs:13:30
   |
12 |     let b = Cons(3, Box::new(a));
   |                              - value moved here
13 |     let c = Cons(4, Box::new(a));
   |                              ^ value used here after move
   |
   = note: move occurs because `a` has type `List`, which does not
   implement the `Copy` trait

The Cons variants own the data they hold, so when we create the b list it moves a to be owned by b. Then when we try to use a again when creating c, we're not allowed to since a has been moved.

We could change the definition of Cons to hold references instead, but then we'd have to specify lifetime parameters and we'd have to construct elements of a list such that every element lives at least as long as the list itself. Otherwise, the borrow checker won't even let us compile the code.

Instead, we can change our definition of List to use Rc<T> instead of Box<T> as shown here in Listing 15-12:

Filename: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use List::{Cons, Nil};
use std::rc::Rc;

fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    let b = Cons(3, a.clone());
    let c = Cons(4, a.clone());
}

Listing 15-12: A definition of List that uses Rc<T>

Note that we need to add a use statement for Rc because it's not in the prelude. In main, we create the list holding 5 and 10 and store it in a new Rc in a. Then when we create b and c, we call the clone method on a.

Cloning an Rc<T> Increases the Reference Count

We've seen the clone method previously, where we used it for making a complete copy of some data. With Rc<T>, though, it doesn't make a full copy. Rc<T> holds a reference count, that is, a count of how many clones exist. Let's change main as shown in Listing 15-13 to have an inner scope around where we create c, and to print out the results of the Rc::strong_count associated function at various points. Rc::strong_count returns the reference count of the Rc value we pass to it, and we'll talk about why this function is named strong_count in the section later in this chapter about preventing reference cycles.

Filename: src/main.rs

# enum List {
#     Cons(i32, Rc<List>),
#     Nil,
# }
#
# use List::{Cons, Nil};
# use std::rc::Rc;
#
fn main() {
    let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
    println!("rc = {}", Rc::strong_count(&a));
    let b = Cons(3, a.clone());
    println!("rc after creating b = {}", Rc::strong_count(&a));
    {
        let c = Cons(4, a.clone());
        println!("rc after creating c = {}", Rc::strong_count(&a));
    }
    println!("rc after c goes out of scope = {}", Rc::strong_count(&a));
}

Listing 15-13: Printing out the reference count

This will print out:

rc = 1
rc after creating b = 2
rc after creating c = 3
rc after c goes out of scope = 2

We're able to see that a has an initial reference count of one. Then each time we call clone, the count goes up by one. When c goes out of scope, the count is decreased by one, which happens in the implementation of the Drop trait for Rc<T>. What we can't see in this example is that when b and then a go out of scope at the end of main, the count of references to the list containing 5 and 10 is then 0, and the list is dropped. This strategy lets us have multiple owners, as the count will ensure that the value remains valid as long as any of the owners still exist.

In the beginning of this section, we said that Rc<T> only allows you to share data for multiple parts of your program to read through immutable references to the T value the Rc<T> contains. If Rc<T> let us have a mutable reference, we'd run into the problem that the borrowing rules disallow that we discussed in Chapter 4: two mutable borrows to the same place can cause data races and inconsistencies. But mutating data is very useful! In the next section, we'll discuss the interior mutability pattern and the RefCell<T> type that we can use in conjunction with an Rc<T> to work with this restriction on immutability.

results matching ""

    No results matching ""