Skip to content
← Back to explainers

Rust explainer

Traits with no methods: Copy, Sized, Send, Sync

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

Understand implicit copying, sized and unsized types, and when values or references can cross thread boundaries.

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

Some traits describe properties without declaring methods of their own. Copy, Sized, Send and Sync are important examples, but they answer different questions:

Trait What it tells you
Copy Assigning or passing the value can implicitly duplicate it.
Sized The type has a size known at compile time.
Send Ownership of the value can safely move to another thread.
Sync Shared references to the value can safely cross thread boundaries.

A trait with no methods of its own is often called a marker trait. Send and Sync are also auto traits: the compiler normally determines them from a type's components. Copy is not automatic for your structs; you opt in when their fields permit it. Sized is determined by the compiler.

Sized: allow different values, not different layouts

An ordinary generic type parameter has an implicit Sized bound. fn f<T>(value: T) therefore requires T: Sized. This does not mean the compiler knows the value, only its type's size.

For example, [u8; 16] has a fixed size. The slice type [u8] and the string slice type str do not encode a length in their type. Trait object types such as dyn std::fmt::Debug are also unsized. References to these types are sized.

Use ?Sized to allow a parameter to be either sized or unsized:

fn byte_size<T: ?Sized>(value: &T) -> usize {
    std::mem::size_of_val(value)
}

assert_eq!(byte_size("hello"), 5); // T is str
assert_eq!(byte_size(&[1_u8, 2, 3]), 3); // T is [u8; 3]

?Sized removes a restriction; it does not require an unsized type. A generic function cannot take an arbitrary unsized T by value on stable Rust, so the example accesses it through &T. Unsized data does not necessarily live on the heap: a slice can borrow part of a stack-allocated array.

The implicit bound also has an important exception: a trait's Self type is not Sized by default. A trait can explicitly require Self: Sized when needed.

References to slices carry length information; references to trait objects carry information used for dynamic dispatch. These are commonly called wide pointers. On common 64-bit targets, &str occupies two machine words, but do not turn that observation into a layout guarantee for every unsized type. ?Sized is a type bound, not a specification of pointer representation.

Copy: implicit duplication has restrictions

#[derive(Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}

let first = Point { x: 2, y: 3 };
let second = first;
assert_eq!(first.x + second.y, 5); // first is still usable

For a struct to implement Copy, every field must be Copy, and the struct must not implement Drop. String and Vec<T> are not Copy; duplicating their ownership information would not create an independent allocation. Shared references are Copy even when the referenced value is not. Mutable references are not Copy.

Copy requires Clone as a supertrait. Consequently, T: Copy also makes Clone::clone available, despite Copy declaring no methods itself. The implication goes one way: String implements Clone without implementing Copy. Deriving both is convenient, but manual implementations are possible.

Copying does not call clone(). For a Copy type, a manual clone should return *self; Clippy's non_canonical_clone_impl checks this convention. The semantic difference between copying and moving is whether the original remains usable, not a promise that either operation emits a machine-level memory copy. Optimization can remove that work.

Send and Sync: ownership and sharing are separate

T: Send permits transferring ownership to another thread. T: Sync means &T: Send, permitting shared access across threads. Neither bound alone implies 'static; thread APIs may impose additional lifetime requirements.

For ordinary structs without an explicit implementation, the compiler checks their fields. A field that lacks an auto trait prevents that trait from being supplied automatically. Library types can have more specific implementations and safety requirements:

Type Send when Sync when
Rc<T> Never Never
Arc<T> T: Send + Sync T: Send + Sync
RefCell<T> T: Send Never
Mutex<T> T: Send T: Send
*const T or *mut T Never Never

Rc uses non-atomic reference counting. Arc uses atomic reference counting, but that protects the count, not arbitrary operations on the value inside. For example, Arc<RefCell<u32>> is neither Send nor Sync. Mutex<RefCell<u32>> can be both: access to the inner value goes through the lock. Choose a wrapper whose guarantees match how the data will be accessed.

Check a bound at compile time

An empty helper can check a positive trait bound without running any work:

fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}

assert_send::<u32>();
assert_sync::<std::sync::Mutex<std::cell::RefCell<u32>>>();

Replacing u32 with std::rc::Rc<u32> makes the first call fail to compile. Keep such intentionally failing examples separate from code expected to run. Projects can use compile-fail tests to verify a rejected bound; an ordinary runtime assertion does not test compiler rejection. This lesson is an article so you can examine both successful and rejected examples.

Manual auto-trait implementations need a safety argument

An unsafe impl Send or unsafe impl Sync is a promise that safe callers can rely on. It can be appropriate for a wrapper around a raw pointer when its ownership, access and destruction rules establish the required guarantee. A lock is not always the answer: exclusive ownership may justify transferring a value, while sharing references requires its own argument.

Do not add an unsafe implementation merely to silence a compiler error. Review every operation the type exposes, including what happens when it is dropped on another thread.

Explicit negative implementations such as impl !Send for LocalHandle {} remain unstable. On stable Rust, a marker field can prevent automatic Send and Sync implementations:

struct LocalHandle {
    id: u32,
    _local: std::marker::PhantomData<*const ()>,
}

The marker occupies no storage and owns no actual pointer. It still affects the enclosing type's auto traits. Use this deliberately when the type must remain on one thread, and document the reason.

step 23 of 24

Generics and Traits

Up next When derived trait bounds are too restrictive