Skip to content
← All articles

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

Four traits you can never call. They carry no behaviour at all — only facts the compiler uses to decide what your code is allowed to do, and they explain a whole family of otherwise baffling errors.

Every trait you have written so far had methods. These four have none:

pub trait Copy: Clone {}
pub trait Sized {}
pub unsafe auto trait Send {}
pub unsafe auto trait Sync {}

There is nothing to call. A bound like T: Copy does not give you a method — it changes the rules the compiler applies at the use site. These are marker traits: their entire content is a fact.

Two of them go further and are auto traits, which means the compiler implements them for you, structurally: a struct is Send if every field is Send, and that is not a convention, it is a derivation the compiler performs on every type in the program.

Sized, and the bound you never wrote

Start here, because it explains more confusing errors than the other three combined.

Every type parameter has an implicit Sized bound. These are the same declaration:

fn f<T>(x: T)          // what you wrote
fn f<T: Sized>(x: T)   // what the compiler read

Sized means “the size of this type is known at compile time”. u64 is sized (8 bytes). [u8; 16] is sized. str, [T] and dyn Trait are not — a str could be any number of bytes and the type alone does not say.

The implicit bound is why fn f<T>(x: T) cannot be called with a str, and it is why ?Sized — the only opt-out in the language — exists:

fn takes<T: ?Sized>(_x: &T) {}    // fine
fn bad<T: ?Sized>(_x: T) {}       // error
error[E0277]: the size for values of type `T` cannot be known at compilation time
  |
2 | fn bad<T: ?Sized>(_x: T) {}
  |        -              ^ doesn't have a size known at compile-time
help: function arguments must have a statically known size,
      borrowed types always have a known size

Read the ? as “maybe”: ?Sized relaxes the requirement rather than adding one. It is the only bound in Rust that works that way. And it must travel with indirection — &T, Box<T>, Rc<T> — because the value itself still cannot live on the stack.

💡size_of::<&u8>() is 8 on a 64-bit machine. What is size_of::<&str>(), and why? click to reveal

Sixteen. So are size_of::<&[u8]>() and size_of::<&dyn Debug>().

A reference to a Sized type is a thin pointer — one address. A reference to an unsized type is a fat pointer — the address plus the metadata needed to make sense of it:

  • &str and &[T]: pointer + length.
  • &dyn Trait: pointer + vtable pointer.

That is the whole implementation of ?Sized. The unsized value lives somewhere; the reference carries the missing half of its type. It also explains two things you may have wondered about: why Box<dyn Trait> is twice the size of Box<ConcreteType>, and why you cannot convert a &dyn Trait back to a thin pointer without losing the vtable.

Copy, and the rule about Drop

Copy says: duplicating this value is a bit-for-bit memcpy, and both copies are equally valid. That single fact changes assignment semantics — a Copy value is copied where a non-Copy value would be moved.

Two hard rules follow.

Clone is a supertrait of Copy. You cannot implement one without the other, which is why #[derive(Clone, Copy)] always appears as a pair.

A type with a Drop impl can never be Copy.

error[E0184]: the trait `Copy` cannot be implemented for this type;
              the type has a destructor
  |
2 | struct S(u8);
  |        ^ `Copy` not allowed on types with destructors

The reason is immediate once stated: Copy means the compiler may duplicate the value silently, and Drop means destroying it runs code. Together they would run the destructor twice on one resource. There is no way to make that sound, so the language forbids the combination outright.

One clippy lint polices the neighbourhood, and it is default-on: clippy::non_canonical_clone_impl. If your type is Copy, its hand-written Clone must be *self and nothing else — anything cleverer is a lie, because the compiler is free to bypass clone entirely and memcpy. Its relative clippy::clone_on_copy catches the caller-side version, x.clone() on a Copy value.

Send and Sync

These two are the foundation of Rust’s “fearless concurrency” claim, and they are best met here, as pure type-system facts, well before you write a thread.

  • Send — this value may be moved to another thread.
  • Sync&T may be shared with another thread. Equivalently: T: Sync if and only if &T: Send.

They are auto traits: the compiler implements them structurally for every type whose fields all implement them. You do not write impl Send for MyStruct and you should not want to — the derivation is automatic and correct.

The interesting types are the ones that opt out:

type Send? Sync? why
Rc<T> no no non-atomic refcount; two threads could race the count
Arc<T> if T: Send + Sync if T: Send + Sync atomic refcount
RefCell<T> if T: Send no the borrow flag is a non-atomic counter
Mutex<T> if T: Send if T: Send the lock makes sharing safe
*const T no no raw pointers carry no safety story

Rc versus Arc is the cleanest illustration. Both are reference-counted pointers; the only difference is whether the count is updated atomically. That one implementation detail is expressed in the type system as Rc: !Send, and so a program that tries to move an Rc across threads fails at compile time:

error[E0277]: `Rc<u32>` cannot be sent between threads safely
  = help: the trait `Send` is not implemented for `Rc<u32>`
💡Why is this an article rather than a problem? What would a test asserting "Rc<u32> is not Send" have to look like, and why can this harness not grade it? click to reveal

Because the assertion is a compile failure, and the harness grades code that runs.

The idiomatic way to assert a positive fact is a static assertion helper:

fn assert_send<T: Send>() {}

fn main() {
    assert_send::<u32>();          // compiles: u32 is Send
    assert_send::<Rc<u32>>();      // E0277 — the program no longer builds
}

The negative version has no passing form. A submission that contains it does not compile, so it cannot pass any test case; a submission that omits it proves nothing. The only way to grade this properly would be a must_not_compile test type — compile the file plus an appended snippet and assert a given error code — which this harness does not have.

There is also no runtime escape. Auto-trait membership is erased before codegen; there is no is_send::<T>() returning a bool, and writing one would need specialisation, which is not stable.

A correction worth recording, since older notes claim otherwise: on clippy 0.1.95 that assert_send helper is accepted, not rejected. clippy::extra_unused_type_parameters deliberately skips functions with an empty body, precisely so the static-assertion idiom stays legal. Add one statement to the body — fn assert_send<T: Send>() { let _ = 1; } — and the lint fires. So the helper is available to you; it is only the negative assertion that has no home here.

unsafe impl, and when you would ever write one

You will occasionally see this in real code:

unsafe impl Send for MyRawWrapper {}

It is unsafe because you are overriding the compiler’s structural derivation. The situation where it is correct is narrow: your type contains a raw pointer (so the auto-derivation says “not Send“), and you have a synchronisation argument the compiler cannot see. Writing it means you have personally taken on the proof obligation. Before Track 17 you should never need one.

The mirror image, opting out, has no stable syntax at all. The workaround is to include a PhantomData<*const ()> field, which drags !Send and !Sync in structurally.

The four facts to keep

  1. Every T secretly means T: Sized. ?Sized is the only opt-out and it must travel with &, Box or another pointer.
  2. Copy: Clone, and Copy + Drop is a compile error (E0184).
  3. Send/Sync are auto traits: derived structurally, never written by hand except with unsafe impl.
  4. Marker traits carry no methods. A bound on one changes what the compiler lets you do, not what you can call.