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:
-
&strand&[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—&Tmay be shared with another thread. Equivalently:T: Syncif 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
-
Every
Tsecretly meansT: Sized.?Sizedis the only opt-out and it must travel with&,Boxor another pointer. -
Copy: Clone, andCopy+Dropis a compile error (E0184). -
Send/Syncare auto traits: derived structurally, never written by hand except withunsafe impl. - Marker traits carry no methods. A bound on one changes what the compiler lets you do, not what you can call.