Should this code be accepted?

The following code is currently rejected by rustc:

trait T<'a> {
    type A;
}

impl T<'_> for () {
    type A = u32;
}

fn foo() -> <() as T<'_>>::A {
    0
}

Playground link

I understand why this errors, as the type signature does technically contain an output lifetime with no input lifetime. However, the actual return type ignores that lifetime, making it unnecessary in this case. Is this a bug in rustc or is this intended behavior?

Here's a version that works:

fn foo(_: &()) -> <() as T<'_>>::A {
    0
}

Can you figure out what the difference is that allows this version to work?

Answer

'_ isn't a lifetime in and of itself. It's a placeholder for "the elided lifetime". A function is eligible for lifetime elision if it takes a single input lifetime. Your foo doesn't have an input lifetime, so it's not a valid candidate for lifetime elision, so there is no '_ lifetime.

What you probably meant to write is instead something like -> for<'a> <() as T<'a>>::A, which would mean the associated type for any possible lifetime. (Again, '_ is one specific lifetime, chosen by the caller via the input lifetime that is being elided.)

'_ in impl T<'_> seems similar to for<'a> 'a, because in that position, the two actually can't be told apart, as they both mean "any arbitrary lifetime". It's only at the function position where the meaning diverges slightly. But both still have their own consistent distinct meaning that they apply in both positions.

That said, the error here isn't helpful, and could probably be improved.

7 Likes

That clarifies some things, thank you. I was not aware of that difference between '_ and for<'a> 'a.

We could consider adding an rule elision for functions with no arguments, but I'm not sure we should. Usually your code just won't work.

Well the reason I was asking was because of a slightly more complicated case that used to ICE, but now errors out properly. In investigating the fix for that ICE I was trying to understand whether the code should be accepted or rejected, and this explanation helped me figure out how to properly reject it. I don't think any new elision rules are needed.

The following also works:

fn foo() -> <() as T<'static>>::A {
    0
}

which I think is actually more close to what you want

1 Like