We can't find the internet
Attempting to reconnect
Something went wrong!
Attempting to reconnect
← Smart Pointers and Interior Mutability step 8 of 26
`Rc<str>`: interning without double indirection
Intern a list of words: every distinct word gets exactly one heap allocation, and every occurrence gets a handle to it.
pub fn intern(words: Vec<String>) -> (Vec<Rc<str>>, usize)
Return one handle per input word, in input order, plus the number of distinct allocations you made.
The grader does not take your word for it. It checks, for every pair of
handles, whether Rc::ptr_eq says they are the same allocation, and it reads
Rc::strong_count on each one after your function has returned. Two equal
words that got separate allocations will fail even though the strings match.
Rc<String> is one pointer too many
Start by counting hops. To read the first byte of an Rc<String> the machine
must:
-
follow the
Rchandle to the refcount block, -
read the
Stringheader stored inside it (pointer, length, capacity), - follow that pointer to the actual bytes.
Two dereferences and two separate allocations, for one string. And a String
is a growable buffer — capacity, spare room, the machinery for push_str —
none of which you can ever use, because an Rc will not hand you &mut.
Rc<str> collapses that. The refcount block holds the bytes themselves,
laid out immediately after the two counters, and the handle is a fat pointer
carrying the address plus the length. One allocation, one hop, and no unusable
growth machinery. For a table of interned strings — which is exactly what an
interner is — this is strictly better on every axis.
The same applies to Rc<[T]> versus Rc<Vec<T>> for shared immutable lookup
tables, and to Box<[T]> versus Box<Vec<T>> when you are done growing a
vector and want to hand it on. Vec::into_boxed_slice exists for that.
The lint that is the curriculum
The first line of this problem’s file is:
#![warn(clippy::rc_buffer)]
rc_buffer is allow-by-default — clippy will never mention it unless you
ask. This problem asks, and since the gate is -D warnings, a warn-level opt-in
is enough to make it fail you. Keep the line; it is part of the exercise.
On the starter it produces:
error: usage of `Rc<T>` when `T` is a buffer type
| map: HashMap<String, Rc<String>>,
| ^^^^^^^^^^
help: try
| map: HashMap<String, Rc<str>>,
Which is a good moment to notice something about lint scope: rc_buffer
inspects struct fields and function signatures, not local let
annotations. If you had written the same type on a local binding it would have
passed silently. Lints see less than you think.
Unsized types behind a pointer
str and [T] are dynamically sized types. You cannot have a bare str
local, because the compiler does not know how many bytes to reserve. But you
can have one behind any pointer that carries the length alongside the address:
| owned | shared | borrowed | |
|---|---|---|---|
| text |
String, Box<str> |
Rc<str> |
&str |
| sequence |
Vec<T>, Box<[T]> |
Rc<[T]> |
&[T] |
All of the right-hand entries are fat pointers — two words, address plus
length. That is the whole mechanism, and it is why Rc<str> needs no special
support: Rc<T> was always generic over T: ?Sized.
Construction is by conversion rather than by Rc::new, because Rc::new
needs a sized value to move in. Rc::from("hello"), Rc::from(s.as_str()),
s.into() where the target type is known, or Rc::from(vec) for Rc<[T]>.
Reaching for Rc::new(s) on a String gives you Rc<String> and then
E0308 when you try to store it where an Rc<str> was expected — which is
exactly the error waiting in the starter.
The price: Rc<str> can never be mutated
Not by get_mut, not by make_mut, not ever. make_mut needs T: Clone and
a place to write a T; str is unsized, so there is no such place. The bytes
are fixed at construction.
For an interner that is not a limitation, it is the point — interned
strings that could change would be a bug. But if you do need shared mutable
text you are back to Rc<RefCell<String>>, with the double indirection and
the runtime borrow flags, and you should ask whether you really need it.
Loading visualization…
Stuck?
Rust reference solution
Sign in to attempt this problem and reveal the reference solution.