Skip to content

Marker traits

Level: 201 · working knowledge

One line: A marker trait has no methods at all — it does not say what a type can do, it says something true about the type, and the compiler enforces it exactly as hard as any other bound.

trait Audited {}          // no methods. Nothing to call. This is the whole trait.

struct Ballot;
impl Audited for Ballot {}

fn publish<T: Audited>(_thing: &T) -> &'static str { "published" }

fn main() {
    println!("{}", publish(&Ballot));   // published
}

A Draft that does not implement Audited cannot be passed to publish, and the failure is E0277 at compile time. No method was added, no byte was added, and a whole category of mistake became unwriteable.

The four you already use

Marker Says
Sized the size is known at compile time
Copy duplicating the bytes is a valid duplication of the value
Send it is safe to move this to another thread
Sync it is safe to share &T between threads

They live in std::marker, which describes itself as "primitive traits and types representing basic properties of types" — a good one-line definition of the whole idea.

Sized: the bound you never wrote

Every generic parameter has it, silently. fn f<T>(x: T) means fn f<T: Sized>(x: T), because a value passed by value needs a size. That default is why ?Sized exists — the one "relaxed" bound in the language, and the only place a ? appears in a bound:

fn width<T: ?Sized>(value: &T) -> usize { std::mem::size_of_val(value) }
// width("hello")  works. Without ?Sized it does not, because str has no size.

This is the same fact that makes ToOwned necessary: Clone requires Sized, str is not Sized, so str has no Clone impl and .clone() on a &str quietly clones the reference instead.

Auto traits: the impls nobody writes

Send and Sync are auto traits. The compiler implements them for your type automatically if every field qualifies, and withholds them if any field does not. You never write impl Send for MyStruct, and in safe code you cannot.

That is why Arc<i32> can cross a thread boundary and Rc<i32> cannot, despite being the same size and holding the same value — Arc's reference count is atomic and Rc's is not. The difference is a promise the compiler is tracking, not a byte in the value. The way to ask about it is to write a function that only compiles for Send types:

fn assert_send<T: Send>() {}
assert_send::<Arc<i32>>();   // compiles
assert_send::<Rc<i32>>();    // E0277

PhantomData: marking the type instead of the impl

Sometimes the thing to mark is a type parameter the struct does not actually store. PhantomData<T> is a zero-sized field that makes the parameter real to the type system and free at run time:

struct Tagged<Unit> { value: f64, _unit: PhantomData<Unit> }
struct Metres;
struct Feet;

Tagged<Metres> and Tagged<Feet> are different types that cannot be added together, and size_of::<Tagged<Metres>>() is 8 — exactly the f64. Same family as the newtype, one level more general: the newtype gives one type a private door, PhantomData gives a whole family of them.

That is the whole idea and not the whole story — the field is a claim about variance and drop rather than a way to silence E0392, and an impl block can be written for one tag alone. Phantom types is the page for both.

The verified output

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

1. A marker trait gates a function without adding a method
   publish(&Ballot) = published
   publish(&Draft)  would be E0277: `Draft: Audited` is not satisfied

2. `Sized` is the marker you never wrote — it is implicit on every T
   width("hello")     = 5   a str: 5 bytes, and str is NOT Sized
   width(&[1i32, 2, 3][..]) = 12   a slice of three i32
   Without `T: ?Sized` on that function, neither call would compile.

3. `Send` and `Sync` are AUTO traits: nobody wrote the impls
   assert_send::<Arc<i32>>()  compiles — Arc's count is atomic
   assert_send::<Rc<i32>>()   does NOT — Rc's count is not
   size_of::<Rc<i32>>() = 8 and size_of::<Arc<i32>>() = 8: the same
   size. The difference is a promise, not a byte.

4. PhantomData marks the TYPE and costs nothing
   Tagged<Metres> 400 · Tagged<Feet> 300
   size_of::<f64>()            = 8
   size_of::<Tagged<Metres>>() = 8   the unit is free
   ...and adding them is a compile error, which is the entire point.

See also

  • Copy vs Clone — the marker trait most people meet first, and the one that changes what = means
  • ToOwned — what Sized being implicit costs str
  • A score is not a number — the same "make it unwriteable" instinct, without generics
  • What a trait is — the ordinary kind, with methods in it

Po polsku

Cecha znacznikowa (marker trait) nie ma ani jednej metody — nie mówi, co typ potrafi, tylko orzeka o nim coś, co jest prawdą. To jeden z niewielu przypadków, w których polskie słowo trafia lepiej niż angielskie: trait bez metod nie jest żadnym „interfejsem”, tylko dosłownie cechą typu, w tym samym potocznym sensie co cecha charakteru. Czytelnik z Javy zna to jako interfejs znacznikowy (Serializable, Cloneable) i warto od razu zobaczyć różnicę: tam znacznik sprawdza się przez instanceof w czasie działania i da się go obejść, tu jest to zwykłe ograniczenie (bound) na parametrze generycznym, więc typ bez niego po prostu się nie kompiluje — E0277, zero dodanych metod i zero dodanych bajtów.

Dwie rzeczy z tej strony sprawiają najwięcej kłopotu. Pierwsza to Sized, ograniczenie, którego nigdy nie napisałeś: każde fn f<T>(x: T) znaczy w istocie fn f<T: Sized>(x: T), bo wartość przekazywana przez wartość musi mieć rozmiar. Dlatego istnieje ?Sized — jedyne miejsce w języku, gdzie w ograniczeniu pojawia się ?, i nie znaczy ono „opcjonalny”, tylko „nie wymagam tego”. Stąd bierze się zaskoczenie z .clone() na &str: Clone wymaga Sized, str nie jest Sized, więc klonuje się referencja, a nie łańcuch znaków. Druga to cechy automatyczne (auto traits) — Send i Sync nadaje kompilator sam, na podstawie pól, i w bezpiecznym kodzie nie da się napisać impl Send for …. Rc<i32> i Arc<i32> zajmują tyle samo bajtów; to, że jeden przejdzie do innego wątku, a drugi nie, jest obietnicą, nie bajtem w pamięci. Zapytać o nią można tylko kompilatora — fn assert_send<T: Send>() {} i wywołanie, które albo się kompiluje, albo nie.

Szukaj po polsku: cecha znacznikowa · interfejs znacznikowy · ograniczenia typów generycznych · rust marker trait · rust ?Sized · rust auto trait Send Sync