Understanding the marker traits

There is one thing right now I can not wrap my head around it, namely the marker traits, even after reading the official documentation and other articles and posts.

As far as I understood, marker traits do not have any behaviors. They are just used to give the compiler certain guarantees. But how does this work exactly? I assume that the compiler has some checks and rules against these types, and if they are violated, they result in compilation errors. Is that correct? So basically, they are only important for compilation?

For example, there is a marker-trait called Send. This marker trait guarantees that I can send/move a value from one thread to another. So here there must be some logic in the compiler which checks this, right? What I do not get is what makes sure that a given value can be safely sent to other threads, in this case.

Thanks in advance for the help!

Yes.

It doesn't. It has rules to automatically infer whether a type is Send. You can implement Send for your own types - its an unsafe impl, so the onus is on the user or the library implementor.

1 Like

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 Sendability; 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 [1]. 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 {}

  1. a marker trait can be dyn unsafe if it has a supertrait that is dyn unsafe -- e.g. Copy ↩ī¸Ž

13 Likes

Another thing that hasn't been mentioned yet is that while Send and Sync do have special behavior as auto traits, the actual thread-safety part is largely handled by library code. In particular, std::thread::spawn has the bounds

pub fn spawn<F, T>(f: F) -> JoinHandle<T> 
where
    F: FnOnce() -> T,
    F: Send + 'static,
    T: Send + 'static, 

Those Send bounds on F (the function sent to the new thread) and T (the value returned from the thread) are what actually prevent you from sending values to a new thread. In general, any thread creation or inter-thread communication tool will have Send bounds on the values it carries.

Many other thread-safety rules are also expressed as trait implementations. For example, the basic relationship between Send and Sync is an impl itself:

unsafe impl<T: Sync + ?Sized> Send for &T {}

And sometimes it can be sneaky; for example, in std::sync::mpsc you can perfectly well use a channel for any type, even a non-Send one, but if the message type isn't Send, you can't send the ends of the channel, so the entire channel thereby can't ever leave a single thread.

unsafe impl<T: Send> Send for Sender<T> {}
unsafe impl<T: Send> Send for Receiver<T> {}

In all of these cases, the compiler's special features are not at all necessary to ensuring thread-safety; what the compiler is doing is creating convenience by implementing Send and Sync when possible for data structures that are made of Send or Sync parts. The language would be fundamentally the same if Send and Sync were not auto traits; but you'd spend a lot more time having to add derives or impls for those traits (and occasionally filing bugs against libraries that forgot them).

11 Likes

Wow, this is exactly the answer I was looking for! Informative, detailed, and pointing out the special cases! A lot of things to learn and digest!

Many thanks for this, highly appreciated!

Thanks a lot for the additional information, much appreciated, @kpreid!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.