Impl Trait return value with associated type that should be Send + Sync

I just learned the following fact:

If you have a function that returns an impl Trait and Trait has an associated type, then this associated type will not implement auto traits like Send or Sync as long as the associated type has no explicit bound to it.

Here is a code snippet that explains the situation:

trait Inner {}

trait Outer {
    type I: Inner;
    
    fn get_inner(&self) -> Self::I;
}

impl Inner for () {}

impl Outer for () {
    type I = ();
    
    fn get_inner(&self) -> Self::I { 
        ()
    }
}

fn outer1() -> impl Outer {
    ()
}

fn outer2() -> impl Outer<I = impl Inner> {
    ()
}

fn requires_send(_: impl Send) {}

fn main() {
    
    // uncommenting the following does not compile!
    // requires_send(outer1().get_inner());
    
    // this does compile!
    requires_send(outer2().get_inner());
}

As we can see it helps if we explicitly have a <I = impl Inner> that doesn't tell the compiler anything really (at least at first glance) but it fixes the problem.

My guess at the moment is that with the explicit <I = impl Inner> the compiler treats the associated type similar to impl return types which normally also implements Send and Sync (if possible), see 1522-conservative-impl-trait - The Rust RFC Book (I didn't find a more up to date explanation of this behaviour).

So my question is: Is this intended compiler behaviour or a bug and is there somewhere a good general explanation of this behaviour?

2 Likes

This might be worth opening an issue on rust-lang/impl-trait-initiative: Impl trait lang team initiative. Everything I looked at seems to suggest that auto trait leakage should be possible without ATPIT. (But maybe it is computationally inhibitive or requires the new trait solver instead of just being an oversight? I think the initiative folks would know more about that.)

1 Like

As far as I understand your discovery, it means right now it's not a breaking change to change the body of outer1 in such a way that the auto-traits of the associated type of the returned type are less. But if the auto-traits start leaking, it will be :upside_down_face:.

(I don't know that anyone consciously relies on that.)


Here's another quirk I found:

fn outer3() -> impl Outer<I = impl Send> {
    ()
}

fn requires_inner<I: Inner>(_: I) {}


    // Fails
    requires_inner(outer3().get_inner())

Found here.

But you can regain this bound.

fn outer4() -> impl Outer<I = impl Inner> {
    fn helper<Out: Outer>(outer: Out) -> impl Outer<I = impl Inner> {
        outer
    }
    helper(outer3())
}

I didn't find anything about your actual OP though.

Thanks for your answer. My problem is that I am not so much concerned about breaking changes but more that I want my return values to be usable in multi-threaded contexts if needed. But on the other hand if a user wants to pass in non-Send stuff to the functions it should also be ok (then the result is also not Send), so I kind of intentionally rely on the leakage.

This means if I want the leakage to happen I always need to explicitly state the associated types because otherwise they won't leak.

In the end it is not a big deal if you know about it, but it feels a bit weird and there is no good documentation about it that explains this behavior (at least I have not found it).

Yeah, I understand. It seems like you care enough that you should make an issue. If the idea is entertained, the change in maintainer flexibility is one of the things to consider.

Another is how important it will be with gen fn types if they end up being based on IntoIterator.

I agree that no matter which way things end up, the behavior should be documented.

Good idea, I just created in issue now: Auto trait leakage on associated types of impl Trait return value · Issue #21 · rust-lang/impl-trait-initiative · GitHub