Where is the place that implements the trait Sync for "& mut T"?

fn test_trait<T:Sync>(_v:T){}

fn test(v:&'static mut i32){
    test_trait(v);  // ok
}

The definition of trait Sync in Sync in std::marker - Rust says

The precise definition is: a type T is Sync if and only if &T is Send.

A somewhat surprising consequence of the definition is that &mut T is Sync (if T is Sync) even though it seems like that might provide unsynchronized mutation.

According to the definition, that means, Sync is implemented for & mut i32 if && mut i32 satisfies Send. By looking at the code in marker.rs

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

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

The condition for implementing Send for &T is T satisfies Sync at least, in this example, that means we implement Send for & & mut i32 if only & mut i32 satisfies Sync. This results in a circular requirement.

So, I didn't see how the conclusion

A somewhat surprising consequence of the definition is that &mut T is Sync (if T is Sync)

gets from the first definition

a type T is Sync if and only if &T is Send.

So, where is the place we implement Sync for & mut T? Is it a compiler internal implementation that didn't appear in the standard library?

1 Like

Correct. Send is what is known as a marker trait, which means that its only purpose is to signal something to the compiler.

1 Like

It's defined by the standard library just like any other trait.

4 Likes

I didn't ask where is the place Send is implemented for & mut T. I asked Sync

1 Like

It seems that you didn't understand my question. I asked where is the place the trait Sync is implemented for type & mut T?

1 Like

Sync is something special called an auto trait. The Sync implementation for &mut T when T is Sync comes from the automatic implementation rules for auto traits.

5 Likes

It seems that it's a compiler builtin, since the auto trait derivation rules are builtins. Quoting the Reference:

  • &T, &mut T, *const T, *mut T, [T; n], and [T] implement the trait if T does.

So the auto trait derivation rules imply that &mut T is Send iff T is Send, and similarly for Sync, unless the stdlib specifies otherwise (it doesn't).

3 Likes

Thanks. This is the answer I'm looking for.

Thanks. This is what I am looking for. By looking at marker.rs

impl<T: ?Sized> !Sync for *const T {} 

impl<T: ?Sized> !Sync for *mut T {}

There are two negative implementations. & mut T is not on the list of negative implementations. That is to say: Sync is implemented for & mut T if Sync has been implemented for T.

In this case, T is i32, then the first definition applies to i32, that is

i32 is Sync if and only if &i32 is Send

And we can find the explicit written item in marker.rs

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

which requires i32 to satisfy Sync, which is specified in Sync in std::marker - Rust. However, I didn't find the implementation of Sync for i32 in marker.rs. This makes the issue turn to the question: where is the place that we implement Sync for i32, or generally say, for a generic T that is a non-reference type?

In your cited link, it says

For generic types (counting the built-in types above as generic over T), if a generic implementation is available, then the compiler does not automatically implement it for types that could use the implementation except that they do not meet the requisite trait bounds. For instance, the standard library implements Send for all &T where T is Sync; this means that the compiler will not implement Send for &T if T is Send but not Sync.

So, it is not clear whether Sync is implemented for T or not, besides it explicitly said in Sync in std::marker - Rust

I'm sure that the implementations for concrete primitive types are provided by the language. Since the semantics of Send and Sync are fixes, it's easy to verify that the primitives should indeed implement them. I don't know whether there could be some benefit from providing the implementations in the stdlib. It could slightly simplify the Send/Sync derivation logic in the compiler, but compared to the rules linked above that's a negligible improvement, and it would allow the stdlib to impl !Send for i32, which would be a semantic violation.

I'd say it's quite clear from the rules above. It may not be easy to track manually, but neither does it usually matter much. It's just a safety guardrail which doesn't affect the semantics. You should almost never implement Send/Sync by hand. In the rare case that you do, it's either obvious because your type contains raw pointers, or you learn about it when you try to pass your type to a function which requires those bounds.

There are exceptions, unfortunately. It is possible, though hard, to write a type which automatically implements Send/Sync, but where that would be unsound. An example is MutexGuard, which is implemented as

pub struct MutexGuard<'a, T: ?Sized + 'a> {
    lock: &'a Mutex<T>,
    poison: Guard,
}

pub struct Guard {
    panicking: bool,
}

The auto trait rules would mean that MutexGuard<T>: Sync, which is invalid for T: !Sync, since &MutexGuard<T> can be converted safely into &T, thus allowing to unsoundly send &T between threads. Nowadays we have an impl

#[stable(feature = "mutexguard", since = "1.19.0")]
unsafe impl<T: ?Sized + Sync> Sync for MutexGuard<'_, T> {}

which prevents the above bug, but as you can see from the above annotation this was only fixed in Rust 1.19. It was one of the achievements of the RustBelt project.

Still, this is only something which you should care about if you are writing unsafe code, which requires a lot of care anyway.

1 Like

For this rule

if a generic implementation is available, then the compiler does not automatically implement it for types that could use the implementation except that they do not meet the requisite trait bounds. For instance, the standard library implements Send for all &T where T is Sync ; this means that the compiler will not implement Send for &T if T is Send but not Sync .

I think they are contradictory. First, it says

if a generic implementation is available, then the compiler does not automatically implement it for types that could use the implementation except that they do not meet the requisite trait bounds.

That means if there exists the implementation and the exception case is met, then the compiler will automatically implement it.

Then, it gives an example

For instance, the standard library implements Send for all &T where T is Sync ; this means that the compiler will not implement Send for &T if T is Send but not Sync .

Since the standard does define the implementation

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

The requirement of the trait bound is T: Sync, the example says T is Send but not Sync. This is exact what the exception case says, so the compiler should automatically implement Send for &T for that T

1 Like

No. The exception case means that if there is impl Send for Foo<T> or impl !Send for Foo<T>, then the compiler will not infer Send or !Send for Foo<T> regardless of trait bounds, other than according to the explicitly specified impls.

If there are no explicit impls, then the compiler infers Send or !Send automatically, according to the rules in the linked section. It means, in particular, than a struct with only Send fields will be unconditionally Send, as with the MutexGuard, unless you provide at least one explicit Send/!Send impl.

IMO, the emphasized rule means, if we have an implementation item

impl Send for Foo<T>{ 
  // ... 
}

and the type T where the implementation implements on satisfies the trait bound, then the compiler will not infer Send for Foo<T> according to the lined section. Otherwise, the compiler will infer that.

@afetisov interpretation is correct, as can be seen here. MySendNotCopy not implementing Copy made MyGeneric<MySendNotCopy> not implement Send. If the rule was that the auto-impl applied when the bounds of the impl weren't met, it would implement Send. (At least, that's how I've interpreted each of your interpretations :sweat_smile:.)

Note that you can't do something like this, but the rules accomplish the same thing.

See also this issue. Right now the rule is "we only discard the builtin implementation if an explicit impl applies to a given type while ignoring where-clauses of that explicit impl." [emphasis added]. In the future the plan is to be even quicker to discard the builtin implementation... once the ability to have overlapping implementations for marker traits, and/or to have negative where clauses, stabilize (thus allowing the same expressiveness).


I recommend this post on Send/Sync more generally.

3 Likes

Your example proves that @afetisov 's interpretation is correct.

struct MySendNotCopy; 

unsafe impl<T> Send for MyGeneric<T> where T: Copy {} 

fn can_send<T: Send>() {}  

can_send::<MyGeneric<MySendNotCopy>>(); // error

However, it may be nit-picking, from the perspective of English, consider this sentence

Precondition: there exists such a generic implementation

The compiler does not automatically implement it for types that could use the implementation except that they do not meet the requisite trait bounds.

My opinion of this sentence is that

If the types do not satisfy the trait bounds, then "The compiler does not automatically implement it for types" will not apply to the types.

Your opinion of this sentence is that

If the types can use the implementation when we ignore any trait bound in that implementation, then "The compiler does not automatically implement it for types" works.

I think the ambiguity arises from where the exception works.

3 Likes

I agree it could be more clear.

Maybe the definition is not precise.
I think maybe it should be:

a type &T is Send if and only if T is Sync.

And,

 a type T is Sync if and only if &T is Send.

is meaningless because you can't make &SomeT: Send to force SomeT: Sync.

struct Y {
    s: i32,
}

unsafe impl Send for &Y {}

will report

can't implement cross-crate trait with a default impl for non-struct/enum type

No. The "if and only if" part makes the relationship symmetric, so in which order you say T and &T doesn't matter.

2 Likes

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.