The Rust Engineering Handbook / Chapter 27
Pointers and Smart Pointers
Choose borrowing, unique heap ownership, reference counting, weak observation, or raw addressing from the ownership graph and safety contract.
Begin with the ownership graph, not the pointer menu
A service configuration is loaded once, and eight workers read it. The first implementation wraps the configuration in Arc<Mutex<Config>>, clones the Arc into every worker, and locks before every read. The program compiles. It has nevertheless encoded three claims that the design may not need: the workers jointly own the configuration, they may outlive the spawning scope, and ordinary reads require runtime coordination.
The repair is not “use a cheaper smart pointer.” Ask who may keep the value alive, who may mutate it, which execution contexts must hold access, and whether an address must remain stable. If all workers finish inside a scoped thread region, they can borrow &Config. If a long-lived task must own an immutable snapshot across threads, Arc<Config> may be exact. If only one component owns it, direct Config ownership is clearer still.
Pointer-choice rule: choose the least powerful pointer that represents the real ownership graph. Add allocation, reference counting, atomics, interior mutability, or unsafe addressing only for an obligation that borrowing or direct ownership cannot express.
This chapter treats pointer types as architectural claims. Chapter 26 separated representation guarantees from observations; the same discipline matters here. A type may look pointer-sized in one build without promising a stable public ABI. A pointer value may carry an address without carrying permission to dereference it. A cloned handle may be cheap relative to cloning the referent while still changing lifetime, contention, and shutdown behavior.
References are temporary capabilities
&T and &mut T are not merely addresses. A reference is a valid, aligned, non-null borrow of a live T for a lifetime, subject to Rust’s aliasing rules. Shared references permit shared access; mutable references provide exclusive access for the duration of the borrow. The compiler can reason about those permissions because the reference type retains them.
Prefer a reference when the callee needs access but must not decide how long the value lives:
fn route(frame: &[u8], config: &Config) -> Route {
config.table.match_frame(frame)
}
The signature says that route borrows a byte view and configuration. It neither allocates nor retains them. A future refactor cannot quietly put either argument into a background task because the returned value does not carry their lifetimes.
A raw pointer, *const T or *mut T, deliberately carries fewer static promises. It may be null, dangling, unaligned, or derived in a way that does not permit the intended access. Creating and moving a raw pointer is safe; dereferencing it is unsafe because the caller must establish validity, alignment, provenance, initialization, aliasing, and lifetime for the operation. Raw pointers are appropriate at FFI edges and inside carefully encapsulated data structures. They are not an escape hatch from a borrow-checker error whose ownership design is still unresolved.
The distinction is operational. A reference in a public API makes misuse difficult at the call site. A raw pointer moves the proof into documentation, tests, and a small unsafe implementation. Review should demand a safety contract that names who created the allocation, when it may move or be freed, which thread may access it, and what synchronization protects mutation.
Direct ownership is the zero-indirection baseline
Owning T directly is often omitted from pointer comparisons, which biases designs toward wrappers. A field such as config: Config gives one enclosing value responsibility for destruction. Moving the owner moves the value unless the compiler can optimize the move away. No heap allocation or reference count is implied.
Direct ownership works well when:
- one component controls the value’s lifetime;
- the value may move safely;
- recursive type size does not require indirection;
- callers can borrow for temporary access;
- independent retention is not part of the API contract.
If a large value is costly to move, first measure. Rust moves are shallow byte moves of the value itself; a String, Vec, or map already owns heap storage through a small handle, so moving the container does not copy its elements. Boxing such a container adds another allocation and pointer chase without making its existing buffer more owned.
Box<T> expresses one owner with heap indirection
Box<T> uniquely owns a value placed in an allocation. Dropping the box drops the value and releases its allocation. Moving the box moves its pointer-like owner, not the pointee. This is useful for recursive types, trait objects, large enum variants whose size has been justified, and values whose allocation boundary is itself part of an interface.
enum Plan {
Step(Operation),
Sequence(Box<[Plan]>),
}
The box breaks recursive size calculation: the compiler knows the size of a box even though a Plan may contain more plans. Box<[Plan]> also communicates fixed-length owned storage after construction, unlike Vec<Plan>, which retains spare-capacity and mutation semantics.
Box<T> is not a general stable-address guarantee. The allocation normally does not move when the Box value moves, but safe code may still move the T out or replace it when the API permits. Self-referential or address-sensitive values need a pinning contract, previewed later. Nor is Box automatically an FFI ownership agreement: converting with Box::into_raw transfers responsibility, and exactly one compatible reconstruction or deallocation must follow.
Allocation has costs beyond the allocator call. It separates related data, consumes allocator metadata, adds a pointer chase, can increase cache and TLB misses, and creates a fallible capacity boundary even when the ordinary API handles allocation failure through the platform’s allocation policy. Indirection can improve a hot enum or collection only when reduced element size outweighs those costs. Benchmark the full access pattern.
Rc<T> makes same-thread ownership plural
Rc<T> puts a value and reference-count bookkeeping in a shared allocation. Rc::clone(&handle) creates another owner of the same value and increments a non-atomic strong count. When the last strong owner is dropped, the value is dropped. Rc<T> is neither Send nor Sync, so the compiler prevents it from becoming a cross-thread sharing mechanism.
Use it when one thread genuinely contains several owners whose lifetimes are not nested cleanly: a GUI model referenced by multiple widgets, an immutable syntax subtree shared by incremental structures, or graph nodes retained by several local indexes. Do not use it merely because lifetimes are inconvenient. Reference counting changes “the parent owns this” into “the last surviving handle owns this,” which may make cleanup time and ownership review less predictable.
Cloning an Rc<T> is not cloning T. It is normally a handle copy plus count update. That is often much cheaper than a deep clone, but it is not semantically free:
- the clone extends the value’s possible lifetime;
- destruction moves to the last drop, which may occur on an unexpected path;
- count traffic can be significant in tight loops;
- shared mutation requires a separate mechanism such as
CellorRefCell; - strong cycles leak the values involved.
An Rc<RefCell<T>> is a precise tool for same-thread shared ownership with dynamically checked mutation. It is also a local runtime aliasing protocol: overlapping mutable borrows panic. If most accesses mutate, if the graph has a clear owner, or if failures must not surface as borrow panics, redesigning ownership is usually preferable.
Arc<T> changes the execution contract
Arc<T> provides shared ownership with atomic reference-count operations so handles may cross threads when T satisfies the required Send and Sync bounds. It does not make T thread-safe. Arc<RefCell<T>> remains unsuitable for cross-thread sharing; Arc<Mutex<T>>, Arc<RwLock<T>>, atomics, immutable snapshots, or message passing represent different mutation protocols.
The cost distinction from Rc matters under contention. An Arc clone and drop update shared atomic state. The exact instructions are implementation and target details, but the architectural consequence is durable: handle churn can cause cache-line traffic between cores. Avoid cloning inside per-item loops when one longer-lived handle or borrow serves the same lifetime. Measure before pooling handles or inventing unsafe alternatives.
The memory-views-lab shows a simpler design when concurrency is scoped:
pub fn scoped_labels(config: &str, worker_count: usize) -> Vec<String> {
std::thread::scope(|scope| {
let handles: Vec<_> = (0..worker_count)
.map(|worker| scope.spawn(move || format!("{config}:{worker}")))
.collect();
handles
.into_iter()
.map(|handle| handle.join().expect("worker must not panic"))
.collect()
})
}
The scope proves that every worker finishes before the borrowed configuration can disappear. There is no shared ownership, no reference-count traffic, and no lock for immutable reads. This does not mean scoped threads always replace Arc: detached work, task queues, cached values returned to independent consumers, and runtime-owned asynchronous tasks may need owned 'static state. The example teaches that concurrency alone does not imply shared ownership.
Replacing an indiscriminate Arc design
Audit Arc<Mutex<Config>> along four axes.
- Lifetime: if workers cannot outlive a scope, borrow
&Config; if a supervisor owns workers until shutdown, let it ownConfigand lend access. - Mutation: if updates are rare, publish immutable
Arc<Config>snapshots through an explicit swap mechanism rather than locking every read. If there is one writer, messages may serialize changes more clearly. - Ownership: if only a registry retains the value, store it there and return temporary references or operation results. Do not let every caller retain the entire configuration accidentally.
- Failure: define shutdown, poisoning, cancellation, and old-snapshot retention. A reference count answers none of them.
Two credible alternatives often beat the original. Borrowing inside a structured lifetime minimizes runtime machinery but cannot escape the scope. Immutable snapshot sharing gives independent readers and lock-free access to a version but can retain several large versions and makes freshness explicit. A dedicated owner plus request messages centralizes mutation and observability but introduces queueing, backpressure, and failure handling. Choose from system behavior, not type familiarity.
Weak<T> models observation, caches, and back-edges
Weak<T> refers to an Rc or Arc allocation without keeping its inner value alive. upgrade returns None after the strong owners are gone. A weak handle may keep allocation bookkeeping alive, but it does not extend the lifetime of T.
The fixture’s tree encodes a clear direction:
pub struct Node {
name: String,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
pub fn attach(parent: &Rc<Node>, child: &Rc<Node>) {
*child.parent.borrow_mut() = Rc::downgrade(parent);
parent.children.borrow_mut().push(Rc::clone(child));
}
Parents strongly own children. Children can inspect parents while they exist, but the reverse edge cannot keep an abandoned tree alive. The test retains a child, drops the root, and verifies that both stored weak observers fail to upgrade.
This is not merely cycle cleanup. Weak makes absence part of the access contract. Callers must decide whether failed upgrade means cache miss, normal shutdown, stale subscription, retry, or invariant violation. Repeatedly upgrading in a hot path also updates or inspects shared bookkeeping; hold a strong handle for the duration of one coherent operation when that is the intended lifetime.
Strong cycles are safe memory leaks rather than use-after-free errors. They can still become production incidents by retaining buffers, file descriptors, task handles, and credentials. Heap profiles, lifecycle counters, and shutdown assertions should reveal objects that outlive their domain. A rule such as “ownership edges point from aggregate to members; parent, listener, and cache edges are weak” gives reviewers something testable.
At this point the family map can be read as an ownership proof rather than a menu of wrappers. Follow the upper path only as far as the lifetime graph requires, then inspect every strong and weak edge in the tree below it.

NonNull<T> is a building block, not safer ownership
NonNull<T> is a raw pointer wrapper that is guaranteed non-null. It is useful inside unsafe collections, allocators, intrusive structures, and FFI wrappers where null is not a valid state. It may still dangle, be misaligned for an intended operation if constructed incorrectly, lack permission for mutation, or point to an invalid value. Its constructors and methods do not create ownership.
The standard library documents Option<NonNull<T>> as having the same size as a raw pointer because the absent state can use null. That narrow guarantee does not justify assuming that every smart pointer, wrapper, or pointer-to-unsized-type is one machine word. Chapter 26’s guarantee ledger still applies.
NonNull<T> is covariant over T, unlike *mut T. An abstraction that exposes mutation and must be invariant needs an appropriate marker such as PhantomData<Cell<T>>. This is where Chapter 25’s variance analysis becomes operational: selecting NonNull can change which lifetime substitutions the enclosing unsafe type accepts.
A defensible wrapper should keep NonNull private and state:
- whether it owns, borrows, or merely observes the allocation;
- allocator and deallocator pairing;
- valid range, alignment, initialization, and provenance requirements;
- aliasing and mutation rules;
- whether null, empty, and dangling sentinels exist;
- drop order and panic behavior;
- thread-safety rationale;
- tests or dynamic tools that exercise the unsafe boundary.
If a safe reference, index, handle, or Box can express the design, prefer it. NonNull reduces representational states; it does not reduce proof obligations.
Identity is a separate question from equality and ownership
Rc::ptr_eq and Arc::ptr_eq compare whether two handles refer to the same allocation. Raw pointer equality compares pointer values under the language’s pointer semantics. Neither implies that values are equal, that the pointee is still live, or that an address is a durable identifier.
Allocator reuse means an address observed after deallocation can later name unrelated storage. Moving directly owned values may change their address. Zero-sized values can share addresses. Wide pointers may include metadata, and Chapter 30 examines those dynamically sized forms in depth. Logs and protocols should use domain identifiers rather than pointer formatting.
Identity comparisons are appropriate for graph algorithms, interning, cycle detection, and caches when allocation identity is explicitly the model. Document that choice. If equality of content is intended, implement or call content equality. If business identity is intended, store a stable key. These three notions diverge during deduplication, reloads, snapshot updates, and serialization.
Deref supports transparent access but can hide architecture
Box<T>, Rc<T>, and Arc<T> implement Deref<Target = T>, enabling method calls on T through the wrapper and deref coercions such as &Arc<String> to &str through multiple steps. This makes pointer wrappers ergonomic. It does not make ownership conversion free, and it should not erase the difference between a borrowed &T and a cloned owner.
For custom types, implement Deref when the type is genuinely a smart pointer with transparent access to its target. Do not use it as inheritance or to expose an arbitrary field. Method resolution can become surprising, the target API becomes part of the wrapper’s effective surface, and changing the target can be breaking. Named methods such as config(), bytes(), or as_session() preserve domain intent.
At call boundaries, accept the capability needed. A function that only reads a Config should usually accept &Config, allowing callers with direct ownership, Box, Rc, or Arc to borrow. Accept Arc<Config> only if the function must retain or transfer shared ownership. That distinction prevents accidental count churn and makes lifetime extension visible in review.
Pinning preview: stable location is not ordinary ownership
Pinning adds a promise that a value will not be moved through the pinned pointer unless the value is Unpin. Pin<Box<T>> combines unique heap ownership with a pinning API; Pin<&mut T> pins a borrowed value for the borrow’s duration. The pointer wrapper and pin contract do different jobs.
Most Rust types are Unpin, so pinning them does not create a useful immovability restriction. Address-sensitive state appears in compiler-generated futures and carefully designed self-referential or intrusive structures. Constructing such state, projecting pinned fields, and dropping it safely require a complete argument; simply placing an ordinary value in Box or Arc is not that argument. Chapter 31 develops pinning after dynamically sized types and collections establish the remaining memory model.
Avoid promising “stable address” as a vague performance property. State which operation requires location stability, when the promise begins, which fields may move, and how destruction occurs. FFI callbacks that retain context pointers, asynchronous state machines, and intrusive links each need different lifecycle evidence.
Representation and optimization claims need narrow wording
On conventional targets, references and many owned pointer handles are small compared with their pointees. But the Rust Reference guarantees only that pointers to sized types have the size and alignment of usize; pointers to dynamically sized types have at least pointer size and currently often carry metadata. Library types publish their own guarantees, including selected null-pointer optimizations.
Do not turn an observed size_of::<Arc<T>>() into a public ABI. Rc and Arc have private allocation layouts and count algorithms. Do not transmute among references, raw pointers, boxes, or reference-counted pointers because their sizes happen to match. Ownership transfer APIs such as Box::into_raw/from_raw and Arc::into_raw/from_raw have exact pairing rules; duplicating reconstruction creates double ownership.
Optimization review should measure:
- allocation count and size distribution;
- cache misses and indirection on the real access path;
- clone/drop frequency for shared handles;
- atomic contention for
Arccounts; - peak retained memory and cycle behavior;
- shutdown latency caused by last-owner placement;
- code and proof complexity of any unsafe alternative.
One pointer word saved is not automatically worth a representation dependency. Conversely, removing millions of atomic handle operations can matter even when allocation count stays constant. Measure the system-level consequence.
Failure modes that compile
- Wrapping immutable scoped data in
Arc<Mutex<_>>and paying ownership plus locking costs for every read. - Replacing a lifetime design with
Rcand making destruction depend on an unreviewed last owner. - Using
Arconly because code is concurrent, although structured borrows suffice. - Assuming
Arc<T>makes a non-SyncTsafe across threads. - Cloning handles inside tight loops when one borrowed handle covers the operation.
- Making both directions of a graph strong and leaking the cycle.
- Treating failed
Weak::upgradeas impossible without a shutdown or cache policy. - Exposing
NonNullfrom a safe abstraction without encoding its lifetime and aliasing rules. - Treating address equality as domain identity.
- Implementing
Derefto simulate inheritance and accidentally exporting a target’s API. - Assuming heap allocation alone pins a value.
- Inferring ABI, null optimization, or transfer compatibility from observed handle size.
Exercise: redraw the ownership graph
Take a service design in which every subsystem receives Arc<Mutex<AppState>>. Produce an ownership review with these artifacts:
- Draw strong lifetime edges separately from temporary borrow edges and observer edges.
- Name the component responsible for constructing, mutating, and destroying each state region.
- Replace at least one shared owner with direct ownership plus borrowing or message passing.
- Split immutable snapshots from mutable coordination state; justify every remaining lock.
- For each remaining
Arc, state why the holder must outlive a borrow scope and whether clone/drop traffic is measurable. - Identify cycles and convert non-owning back-edges to
Weak, including the policy for failed upgrade. - If raw pointers remain, write their validity, provenance, aliasing, thread, and destruction safety contract.
- Compare the original and revised designs on allocation count, count operations, contention, peak retention, shutdown order, and failure observability.
The revised design is not automatically better because it contains fewer Arcs. It is better only when its ownership graph matches runtime responsibilities and its added coordination mechanisms have smaller, clearer costs.
Ownership-graph review card
- Can the value be owned directly and borrowed temporarily?
- Does heap indirection solve recursive size, trait erasure, move cost evidence, or a real allocation boundary?
- Who may keep the value alive independently?
- Is sharing confined to one thread or required across threads?
- Does mutation require exclusivity, interior mutability, locking, copy-on-write, atomics, or a dedicated owner?
- What does cloning copy: the referent, a buffer handle, or a reference-counted owner?
- Where does the last strong owner drop, and can that path tolerate destruction cost?
- Which graph edges are ownership and which are observation?
- What happens when
Weak::upgradefails? - Are pointer identity and domain identity being confused?
- Does a raw or
NonNullfield have a complete safety and drop contract? - Is stable address actually required, and is pinning the relevant contract?
- Are size and null-optimization claims documented guarantees or scoped observations?
What to carry into data APIs
Pointer selection begins with responsibility. Borrow when access is temporary, own directly when one component controls lifetime, use Box for justified unique indirection, choose Rc or Arc only for genuine plural ownership in the appropriate execution domain, and use Weak for observers and back-edges that must not retain the value. Raw pointers and NonNull belong behind explicit unsafe contracts.
The same ownership graph governs byte and text APIs. A slice is a borrowed view, a String is an owned UTF-8 buffer, and a parser can return views only while its input owner remains live. Chapter 28 adds boundary validation and encoding: before choosing whether to borrow or own data, an API must decide what its units mean.
Sources and version notes
- Rust Reference: pointer and reference layout
- Standard library:
Box - Standard library:
Rcandrc::Weak - Standard library:
Arcandsync::Weak - Standard library:
NonNull - Standard library:
DerefandPin - The
memory-views-labuses Rust 2024 Edition, was written for pinned Rust 1.97.0, and declares Rust 1.85 as its MSRV. Its count behavior tests exercise documented ownership semantics; it does not expose private allocation layout or instruction sequences as guarantees.
Continue reading
Full table of contents