Skip to content
← Back to explainers

Rust explainer

Deref, coercion and smart-pointer APIs

About 6 min to read. Allow extra time to work through examples.

How dereferencing exposes a borrowed view, why mutable access needs a separate trait, and when implicit access belongs in an API.

This article uses Rust. You can make it your practice mode to browse related content across the site.

You can call len() on an Rc<Vec<T>> or trim() on an Rc<String> even though those methods belong to the contained values. Rust follows dereference implementations when looking for methods and when converting certain borrowed arguments. This article separates those conveniences from ownership transfer.

Follow a borrowed view

Deref has an associated Target type and a method with the signature fn deref(&self) -> &Self::Target. The target may be unsized, such as str or [T]. The returned reference borrows from self; it does not transfer ownership of the target.

This complete example uses several standard implementations:

use std::cell::RefCell;
use std::rc::Rc;

fn trimmed_len(text: &str) -> usize {
    text.trim().len()
}

fn main() {
    let text = Rc::new(String::from(" hello "));
    assert_eq!(trimmed_len(&text), 5);
    assert_eq!(text.trim(), "hello");

    let words = Rc::new(vec![String::from("a"), String::from("bb")]);
    assert_eq!(words.len(), 2);
    assert_eq!(words.iter().map(String::len).sum::<usize>(), 3);

    let boxed: Box<[u8]> = vec![1, 2, 3].into_boxed_slice();
    assert_eq!(boxed.len(), 3);

    let cell = RefCell::new(vec![4, 5]);
    let guard = cell.borrow();
    assert_eq!(guard.first(), Some(&4));
}

At the trimmed_len call, the expected &str allows a chain of deref coercions: &Rc<String> to &String, then to &str. The data is borrowed, not cloned. Coercions occur at designated sites, such as function arguments and bindings with an explicit expected type; Rust does not apply this chain indiscriminately to every expression.

Method calls have their own lookup process involving automatic dereferencing and borrowing of the receiver. It considers candidate receiver types and their shared and mutable references, with unsizing also possible. Lookup can stop at a method on the wrapper rather than reaching the innermost value. Use an explicit type or qualified call when a name collision is confusing. The method-call reference describes the candidate order in detail.

Mutable access needs DerefMut

DerefMut extends Deref with fn deref_mut(&mut self) -> &mut Self::Target. It supports mutable access through the wrapper. Box<T> and RefMut<'_, T> implement it; Rc<T> does not.

Check your understanding Why doesn't a mutable Rc handle give mutable access to its value? Show answer

Other Rc handles may share the allocation. Exclusive access to one handle does not establish exclusive access to the contained value. Unconditionally returning &mut T while aliases remain would violate Rust's borrowing rules.

For a uniquely held Rc, Rc::get_mut can return mutable access after checking that no other Rc or Weak pointers share the allocation. For cloneable data, Rc::make_mut can instead provide copy-on-write behavior. Those explicit APIs handle cases that an unconditional dereference cannot assume away.

A RefCell checks borrowing before it creates a guard. A mutable guard then provides mutable dereferencing for the duration of that borrow:

use std::cell::RefCell;
use std::rc::Rc;

fn main() {
    let cell = RefCell::new(vec![1]);
    {
        let mut guard = cell.borrow_mut();
        guard.push(2);
        assert!(cell.try_borrow().is_err());
    }
    assert_eq!(&*cell.borrow(), &[1, 2]);

    let mut unique = Rc::new(vec![3]);
    Rc::get_mut(&mut unique).unwrap().push(4);
    let shared = Rc::clone(&unique);
    assert!(Rc::get_mut(&mut unique).is_none());
    Rc::make_mut(&mut unique).push(5);
    assert_eq!(&*shared, &[3, 4]);
    assert_eq!(&*unique, &[3, 4, 5]);
}

borrow_mut() may panic if another borrow conflicts; try_borrow_mut() reports failure instead. Once the guard exists, dereferencing it is not a second request to borrow the cell.

Dereferencing is not generally a move

The * operator identifies the target place. What happens next depends on the context and the target type: it may be borrowed, copied or, where Rust permits it, moved. Reading an i32 through an Rc copies the number. Trying to move a String out through an Rc fails with E0507.

An owned Box<T> has special language support for moving out its contents. That behavior does not follow merely from implementing Deref yourself:

use std::rc::Rc;

fn main() {
    let number = Rc::new(7_i32);
    let copied = *number;
    assert_eq!(copied, 7);
    assert_eq!(*number, 7);

    let boxed = Box::new(String::from("owned"));
    let moved: String = *boxed;
    assert_eq!(moved, "owned");

    let text = Rc::new(String::from("shared"));
    let cloned: String = (*text).clone();
    assert_eq!(cloned, "shared");
    let recovered = Rc::try_unwrap(text).unwrap();
    assert_eq!(recovered, "shared");
}

Cloning the inner String creates an independent value. Cloning the Rc creates another handle to the same value. Rc::try_unwrap can recover ownership when no other strong Rc handles remain; it is a separate ownership API, not a coercion.

Trait bounds do not automatically follow the target

A generic T: SomeTrait requirement is checked for the actual T. An Rc<Concrete> does not acquire every trait that Concrete implements just because methods can be found through dereferencing.

This example explicitly borrows the contained String so inference selects T = String:

use std::rc::Rc;

trait Label {
    fn label(&self) -> &str;
}

impl Label for String {
    fn label(&self) -> &str { self.as_str() }
}

fn show<T: Label>(value: &T) -> &str {
    value.label()
}

fn main() {
    let text = Rc::new(String::from("hello"));
    assert_eq!(show(&*text), "hello");
}

Changing the call to show(&text) infers T = Rc<String> and fails because that type has no Label implementation. Explicitly specifying show::<String>(&text) provides an expected reference type and allows coercion. A by-value generic API needs a different solution: borrowing the target will not satisfy a trait bound that is implemented only for its owned type.

Treat Deref as part of your public API

Smart pointers are a common use, but String to str and Vec<T> to [T] are useful examples too. The question is whether implicit access to the target matches the abstraction. Keep dereferencing cheap and unsurprising; the trait itself does not enforce a cost bound or prevent a custom implementation from panicking. See the standard-library guidance.

For an inventory with validation rules, exposing selected methods may be clearer than dereferencing to its internal vector. Deref alone permits shared access such as len() and iter(); it does not permit clear(), sort() or drain() through a mutable vector reference. Adding DerefMut would make those operations available and could bypass your intended update methods. Shared methods can matter too if the target uses interior mutability.

Target methods can collide with wrapper methods as either API evolves. Removing a public dereference implementation can break existing callers. These are design tradeoffs, not something a lint can decide for you.

An explicit accessor or AsRef can offer a borrowed view without implicit method lookup. Borrow has an additional contract: equality, ordering and hashing must agree between the owned and borrowed forms where those traits apply. It is not interchangeable with AsRef for an arbitrary field view. See the Borrow documentation.

Check your understanding When is &[T] a better parameter than &Vec<T>? Show answer

Use a slice when the function needs only the elements. It accepts vector borrows, arrays and subslices. A vector reference additionally exposes vector-specific information such as capacity, so changing it is appropriate only when the function does not need that information. The same reasoning often favors &str over &String and &T over &Box<T>.

Clippy's ptr_arg and borrowed_box can suggest narrower input requirements; explicit_auto_deref can identify unnecessary explicit dereferencing. Read each suggestion in context. The goal is an API that asks for the capabilities it uses, with ownership and mutation rules that callers can predict.

step 19 of 26

Smart Pointers and Interior Mutability

Up next OnceCell: write-once interior mutability