There's a few different ideas going on here.
In one sense, a marker trait is just a trait that doesn't have any items. Even without any special compiler support, this can sometimes be useful (e.g. sealed traits.)
In another sense, there is an unstable #[marker]
attribute that you can put on marker traits (in the above sense) in order to opt into the overlapping implementations of RFC 1268 (also unstable).
And yet another sense are "the things in std::marker
". Most of those things are marker traits, many are also auto traits (another unstable feature), and pretty much all of them have special compiler behavior.
Send
, Sync
, and Unpin
are auto
traits, meaning that if a struct contains fields which all implement the trait, the struct also implements the trait automatically. That's the extent of the checking. However, you can opt out of the trait by implementing !Send
, !Sync
, or !Unpin
. You can also opt out of Unpin
by putting a PhantomPinned
into your struct (as it is !Unpin
). The concept of auto
traits may someday be less special (i.e. stable). On the other hand, I've seen some skepticism around making that stabilization; time will tell.
Copy
is not an auto-trait, but the ability to implement Copy
works similarly -- all your fields must also be Copy
. Also, you cannot implement Copy
for types that implement Drop
. It has additional special (language level) behavior in that moves of Copy
values are not destructive (you can still use the original value).
Sized
is an intrinsic property of types that the compiler exposes via the trait. Most places you declare generic parameter have an implicit Sized
bound. You can remove the bound by using a ?Sized
"bound". Sized
is also used to signal "non-dyn Trait
", though in my opinion that's a hack and distinct compiler-backed marker trait would be a better solution.
I believe all the other experimental marker traits are implementation-detail mechanics to implement language features like unsizing (e.g. from an array to a slice, or a base type to a dyn Trait
), pattern matching (which cannot rely on the implementation of the Eq
trait for example), etc. Sometimes checking the documentation can still help explain language behavior (e.g. why you can't coerce a deeply nested type into a dyn Trait
).
There are other auto and/or marker traits not in std::marker
as well, like UnwindSafe
.
There are other non-marker traits that have special roles in the language, like Drop
.
PhantomData<T>
is a marker type with special compiler behavior around "act like it contains a T
". It's a marker as it is zero sized, doesn't effect alignment, passes through important standard traits so that you can still derive
them, etc.
Sort of, depending on what you mean. There's no runtime check for Send
ability; the implementation of the trait (or not) is a compile-time determination.
But there is no rule against a marker trait being turned into a dyn Trait
if it is dyn
safe . This can be something that you interact with at runtime, e.g. when downcasting.
And in fact, auto
traits are special here too, in that you cannot have a dyn NonAutoOne + NonAutoTwo
, but you can have a dyn NonAuto + Send + Sync + Unpin + AnyNumberOfAutoTraits
. And a dyn Error
is a different type than a dyn Error + Send + Sync
.
Also, again because a marker trait can still have supertraits or other bounds, it can still be an indicator that you can call certain methods, say.
pub fn f<T: Copy>(t: T) -> T{
t.clone()
}
(I guess that is part of what is meant by "important [outside of] compilation" anyway.)
This can be useful when you have a complicated bound you don't want to repeat a lot.
pub trait DoesALot: This + That + Clone + Send + Deref {}
impl<T: This + That + Clone + Send + Deref> DoesALot for T {}