Skip to content

Smart pointer claims, run

Smart pointers › Claims, run

Level: 201 · a companion to the section

One line: Explanations of smart pointers get the idea right and one word wrong — "boiled away", "always owns", "raw pointers such as Rc", "without fuss" — and each of those words is checked below on rustc 1.98.0, by a program or by the compiler's refusal.

The six claims come from introductions to smart pointers of the kind that circulate in articles and course notes; they are quoted in short, as claims. The last section checks the building blocks Rust in Action names in §6.2.3.

Six claims, run

# Claim Verdict Evidence
1 smart pointers are a zero-cost abstraction because they are a compile-time construct the compiler boils away no — the handle is as small as a raw pointer, but counts, flags and allocations happen at run time section 1; What a smart pointer costs
2 String and Vec<T> are smart pointers yes, in std's sense: both Deref and Drop. They hold a raw pointer, not a reference to the buffer section 2
3 a smart pointer owns the data it refers to usually — Cow::Borrowed owns nothing and a Weak keeps nothing alive section 3; What makes a pointer smart
4 some raw pointers, such as Rc<T>, offer shared ownership no — Rc is a smart pointer; Rc::as_ptr is what hands you the raw one section 4, and the E0308 below
5 smart pointers implement Deref, so you can use one in place of its referent without fuss for method calls and coercions; operators and patterns still need the *, and Weak has no Deref section 5, E0369 and E0308 below
6 Rc is single-threaded yes, and the compiler enforces it: Rc<T> is not Send section 6, E0277 below

The verified output

Verified output of smart_pointer_claims_checked.rs — regenerated by tools/run_examples.py, never hand-typed.

1. "A compile-time construct the compiler boils away"
   strong_count before clone 1, after 2
   RefCell borrow_mut while borrowed: Err(BorrowMutError)
   A count that changes and a borrow refused at run time are state the
   program carries. The allocations are counted on the costs page.

2. "String and Vec<T> are smart pointers"
   String derefs to str, Vec<u8> to [u8]
   needs_drop: String true, Vec<u8> true
   String: 'static true, Vec<u8>: 'static true
   Deref and Drop: smart pointers in the sense std's Deref docs use.
   Neither has a lifetime parameter, so the buffer is not held by a
   reference: std stores a raw pointer (a NonNull) and frees it in Drop.

3. "A smart pointer owns the data it refers to"
   Cow::Borrowed(&text) is Borrowed: true
   a Weak outliving its value: upgrade() = None
   Cow::Borrowed owns nothing and a Weak keeps nothing alive; The Book
   says smart pointers own their data "in many cases".

4. "Some raw pointers, such as Rc<T>, offer shared ownership"
   Rc::as_ptr(&counted) is a *const u8
   needs_drop: Rc<u8> true, *const u8 false
   size_of Option<Rc<u8>> 8, Option<*const u8> 16
   Rc is a smart pointer built on a NonNull. A raw pointer is what
   Rc::as_ptr hands you: no count, no drop, and nullable.

5. "Use a smart pointer in place of its referent without fuss"
   boxed.checked_add(1)  Some(42)   a method: found through Deref
   *boxed + 1            42         an operator: needs the *
   `boxed + 1` is E0369. Method calls and coercion sites deref for you;
   operators and patterns do not.

6. Rc is "single-threaded"; Arc is the one that crosses
   thread read Arc<String>: len 4
   strong_count after join: 1
   The same spawn with an Rc is E0277: Rc<String> cannot be sent
   between threads safely.

Refused by the compiler

Claim 4 — an Rc is not a raw pointer.

use std::rc::Rc;

fn main() {
    let counted = Rc::new(5u8);
    let raw: *const u8 = counted;
    println!("{}", raw.is_null());
}
Real rustc 1.98.0 output — rc_as_a_raw_pointer.rs, --edition 2024
error[E0308]: mismatched types
 --> rc_as_a_raw_pointer.rs:5:26
  |
5 |     let raw: *const u8 = counted;
  |              ---------   ^^^^^^^ expected `*const u8`, found `Rc<u8>`
  |              |
  |              expected due to this
  |
  = note: expected raw pointer `*const u8`
                  found struct `Rc<u8>`

A &u8 would have coerced to *const u8 on that line; an Rc<u8> does not. Section 4 shows the other differences: an Rc needs dropping and a raw pointer does not, and Option<Rc<u8>> is 8 bytes where Option<*const u8> is 16, because Rc holds a NonNull (alloc/src/rc.rs in the 1.98.0 source).

Claim 5 — operators do not deref for you.

Real rustc 1.98.0 output — box_plus_one.rs, --edition 2024, abridged
error[E0369]: cannot add `{integer}` to `Box<i32>`
 --> box_plus_one.rs:3:26
  |
3 |     println!("{}", boxed + 1);
  |                    ----- ^ - {integer}
  |                    |
  |                    Box<i32>
  |
note: `Box<i32>` does not implement `Add<{integer}>`

And neither do patterns:

fn main() {
    let boxed = Box::new(41i32);
    match boxed {
        41 => println!("forty-one"),
        _ => println!("something else"),
    }
}
Real rustc 1.98.0 output — box_in_a_pattern.rs, --edition 2024
error[E0308]: mismatched types
 --> box_in_a_pattern.rs:4:9
  |
3 |     match boxed {
  |           ----- this expression has type `Box<i32>`
4 |         41 => println!("forty-one"),
  |         ^^ expected `Box<i32>`, found integer
  |
  = note: expected struct `Box<i32>`
               found type `{integer}`
help: consider dereferencing to access the inner value using the `Deref` trait
  |
3 |     match *boxed {
  |           +

The help line is the fix, and names the trait. Weak's missing Deref is E0614, shown on What makes a pointer smart.

Claim 6 — Rc cannot cross a thread.

use std::rc::Rc;
use std::thread;

fn main() {
    let shared = Rc::new(String::from("sent"));
    let mine = Rc::clone(&shared);
    let handle = thread::spawn(move || mine.len());
    println!("{}", handle.join().unwrap());
}
Real rustc 1.98.0 output — rc_across_a_thread.rs, --edition 2024, abridged
error[E0277]: `Rc<String>` cannot be sent between threads safely
 --> rc_across_a_thread.rs:7:32
  |
7 |     let handle = thread::spawn(move || mine.len());
  |                  ------------- -------^^^^^^^^^^^
  |                  |             |
  |                  |             `Rc<String>` cannot be sent between threads safely
  |                  |             within this `{closure@rc_across_a_thread.rs:7:32: 7:39}`
  |                  required by a bound introduced by this call
  |
  = help: within `{closure@rc_across_a_thread.rs:7:32: 7:39}`, the trait `Send` is not implemented for `Rc<String>`

Rc's counts are plain Cell<usize>, and two threads updating one without synchronization would lose counts. Section 6 does the same spawn with Arc, whose counts are atomic, and the strong count is back to 1 after the join. Send and Sync is the page for the traits.

From Rust in Action §6.2.3

The book's "Smart pointer building blocks" names the types Box, Rc and Vec are built from. Against the 1.98.0 source (rust-src):

The book names In 1.98.0
core::ptr::Unique, the basis of String, Box<T> and Vec<T> exists, and Box<T> is still (Unique<T>, A); it is #[doc(hidden)] and unstable under ptr_internals, and its attribute says to use NonNull instead
core::ptr::Shared, the basis of Rc<T> and Arc<T> does not exist; Rc<T> holds a NonNull<RcInner<T>>
std::rc::Weak and std::arc::Weak std::rc::Weak, and std::sync::Weak — there is no std::arc module
alloc::raw_vec::RawVec, under Vec<T> and VecDeque<T> exists as pub(crate), so it cannot be named outside alloc

The stable building block for your own smart pointer is NonNull<T> ↗, which Raw pointers introduces. The pointer half of the same chapter is checked in Rust in Action, chapter 6: pointer claims, run.

Where this leaves you

Three tests work on anything else you read about smart pointers:

  1. Does it say where the work happens? A count, a flag or an allocation is run-time work. "Zero-cost" is true of the handle's size and of Box's one allocation compared with malloc — not of Rc compared with &T.
  2. Does it say "owns"? Check the type: Cow::Borrowed, Ref, MutexGuard and Weak do not own what they point at.
  3. Does it call a smart pointer a raw pointer, or a reference? All three are pointers; only one of them is unsafe to read.

See also

Sources

std's Deref ↗ (the "often called smart pointers" sentence), Rc ↗ and std::rc ↗ ("Single-threaded reference-counting pointers"); The Book, ch. 15 ↗; Rust in Action, Tim McNamara, Manning 2021, §6.2.3. Video: Rc and RefCell Smart Pointers - Rust ↗ (danlogs) walks through Rc<RefCell<T>>, the shape Interior mutability outlines.

Po polsku

Opisy inteligentnych wskaźników zwykle dobrze oddają ideę i mylą się w jednym słowie. Sprawdzone na rustc 1.98.0:

  • „kompilator je usuwa, więc są darmowe” — nie: uchwyt ma rozmiar surowego wskaźnika, ale liczniki Rc, flaga RefCell i alokacje działają w czasie wykonania;
  • „String i Vec<T> to inteligentne wskaźniki” — tak, w sensie dokumentacji std (Deref i Drop); bufor trzymają przez surowy wskaźnik, nie przez referencję;
  • „inteligentny wskaźnik posiada dane” — zwykle, ale Cow::Borrowed i Weak nie;
  • „surowe wskaźniki, takie jak Rc<T>” — nie: Rc to inteligentny wskaźnik (E0308 przy próbie przypisania do *const u8), a surowy daje dopiero Rc::as_ptr;
  • „można go używać zamiast wartości bez kłopotu” — dla metod i koercji tak; operatory (E0369) i wzorce w match (E0308) wymagają *;
  • „Rc jest jednowątkowy” — tak, i kompilator to egzekwuje (E0277); między wątki idzie Arc.

Szukaj po polsku: inteligentne wskaźniki mity · Rc a Arc wątki · rust Rc is not Send · rust smart pointer zero cost