Why can lifetime arguments be omitted when explicitly specifying arguments in turbofish?

fn foo<'a,T>(){
    
}
fn main() {
  foo::<i32>();
}

The first generic parameter is a lifetime parameter; however, in the call site, only the type parameter is specified, which does not correspond to the lifetime parameter, and the specified type argument doesn't match the lifetime parameter. Paths - The Rust Reference says:

The order of generic arguments is restricted to lifetime arguments, then type arguments, then const arguments, then equality constraints.

So, why is this code ok?

I couldn't find anything in the Specification, but the FLS states the following (my emphasis) on generic parameters conforming to generic arguments:

What 'a is inferred to be in the foo::<i32>(); call I do not know.

I recommend reading the Early vs Late bound parameters section of the Rust Compiler Development Guide. Any late-bound lifetime parameter can never be explicitly assigned. The link talks about what qualifies a lifetime parameter to be late bound. Late-bound lifetime parameters are typically associated with higher-ranked types with the quirk that "unused" lifetimes are also late bound. In your specific example, it's not that you can omit the lifetime argument but you must omit it since it's late bound.

// This
fn foo(){}
// has the same (unnameable) type, `fn() {foo}`, as
fn foo<>(){}
// which has the same type as
fn foo<'a>(){}
// which has the same type as
fn foo<'a, 'b, '…, '∞>(){}
// and all can have their nullary "function constructors" invoked explicitly:
foo::<>;
// or implicitly:
foo;
// This
foo::<'static>;
// nor
foo::<'_>;
// will compile for any of the `foo`s.

The foos with lifetime parameters are silly since they are never used. A more realistic example of a late-bound lifetime parameter is a function that is constrained by its arguments:

// This
fn foo(_: &()){}
// is the same as
fn foo<'a>(_: &'a ()){}
// Again, this
foo::<'static>;
// and
foo::<'_>;
// won't compile.

While those foos also have late-bound lifetime parameters, they have the higher-ranked type for<'a> fn(&'a ()) {foo}.

When a lifetime parameter is early bound, you are allowed to explicitly pass in an argument:

// This
fn foo<'a: 'a>(){}
// has type `fn() {foo::<'_>}`.
// This compiles just fine since `'a` is early bound:
foo::<'static>;
foo::<'_>;
// Interestingly even though `'a` is unconstrained as well, you still don't
// _have to_ pass in an argument unlike an unconstrained "normal" type or
// `const` parameter:
foo;
foo::<>;

As far as "why" we can't pass in lifetime arguments for late-bound parameters, I can't answer that as that would seem to require someone from the lang or compiler teams. Presumably it is because while lifetimes behave a lot like "normal" types, their intent is to only be used in a way that guarantees soundness in terms of memory safety; thus while they are currently modeled similarly as "normal" types, I can see the team not wanting to (ab)use them like they are normal types.

What lifetime argument ends up being "used" shouldn't matter since the compiler only cares that the code is sound. If it chose a temporary lifetime, your code would be fine. If it chose a 'static lifetime, your code would be fine. Whatever it does choose doesn't affect the machine code that is generated, so one could argue that it's more "philosophical" to ponder what ends up being chosen than logical/mathematical/technical.

1 Like

Is your question only about that -- what the reference says? In that case, the code is okay because the reference is incomplete here. @philomathic_life has pointed out that lifetimes are not always turbofishable -- your foo<'a> is a late-bound lifetime that doesn't actually parameterize the function item type.

But even when all lifetimes are turbofishable, you can elide lifetimes when you turbofish type and const parameters, and vice-versa. It's like they're two distinct lists as far as elision goes.

The reference is not only incomplete here, it is incorrect that const arguments must follow type arguments. That was the design in the const parameter RFC, but during development it was decided to let type and const parameters to intermix. That note in the reference was probably based on the RFC or such.

I also don't know why they're talking about equality constraints here. Someone copy-pasted the rules for trait object generic parameter lists or something?


Why are things this way? Sometimes one needs to turbofish a type parameter, but there is no suitable lifetime that you can name. Note that the only nameable lifetimes are those that last at least just longer than the function body. And even if that wasn't a factor, one usually wants the lifetimes to be inferred and not specified anyway. Consider:

use std::cell::Cell;

// Early bound due to appearing in the return type
// I.e. you can turbofish this lifetime
fn foo<'a, T>() -> Vec<Cell<&'a T>> {
    vec![]
}

fn example<'lt>() {
    let mut v = foo::<String>();
    let local = Default::default();
    v.push(Cell::new(&local));
}

Any nameable lifetime ('lt, 'static) will cause an error here. You could do:

    let mut v = foo::<'_, String>();

...but "only" since Rust 1.26.

And even if that weren't the case, I don't think it would have ended up differently -- since people generally want inference, it would be really annoying to have to type

some_call::<'_, '_, StuffICareAbout>(...)

every time a turbofish was required.

Here's a very simple take:

// These don't work
let _ = Vec::<'static, T>::new();
let _ = Range::<'static, usize> { start: 0, end: 0 };

...so why should it be possible to turbofish a lifetime onto a function item that isn't actually parameterized by a lifetime (i.e. the lifetime is late bound)?

There's probably deeper reasons,[1] but that's how I think of it on the surface. It does require understanding the late/early bound topic, so I guess it may not be a typical take.


  1. last I knew there were some late bound lifetimes you could turbofish if you disabled a future compatibility warning ↩︎

1 Like

last I knew there were some late bound lifetimes you could turbofish if you disabled a future compatibility warning

Yeah, the link—which you provided me a few days ago—that I provided goes over an example; but it's not that you can provide a lifetime but instead that additional lifetimes are ignored unless you're talking about something else in which case I'd be interested in seeing an example. For example,

fn main() {
    // Won't compile since one may incorrectly think they are
    // providing an elided lifetime for `'b` when in fact it's simply ignored.
    foo::<'static, '_>;
    // Compiles now, but won't in the future to prevent people
    // from ever passing in explicit lifetime arguments whenever
    // there is at least one late-bound lifetime parameter.
    foo::<'static>;
    // Even without early-bound lifetime parameters, the compiler
    // allows this for inherent methods for now but won't in the
    // future. `'static` is ignored and not actually used
    // for `'a`.
    A.foo::<'static>();
}
struct A;
impl A {
    fn foo<'a>(self) {}
}
fn foo<'a: 'a, 'b>() {}
1 Like

My question is about why we can skip the lifetime parameter to specify the type arguments. In other words, the specified type argument does not match the generic parameter at the first position; the generic parameter at the first position expects a lifetime argument but we specify a type argument at the position.

Because when it comes to elision, it's like you have two lists, one for lifetimes (which must come first) and one for non-lifetimes.

fn foo<'a: 'a, T>() {}

foo();                    // ~ foo::<><>()
foo::<'static>();         // ~ foo::<'static><>()
foo::<String>();          // ~ foo::<><String>()
foo::<'static, String>(); // ~ foo::<'static><String>()

Do you mean, the specified arguments in the turbofish syntax can be automatically paired to the first corresponding/matched category generic parameter that has not yet been paired, then any remaining generic parameters that do not have explicitly specified arguments will be inferred or acquired from the default arguments, as said in jofas's answer?

They're not paired by category exactly. Using the terminology of my last analogy, type and const parameters are in the same list. If you have...

fn foo<T, const U: usize>() {}

...you can...

    // Elide everything
    foo([42]);
    // (Same thing)
    foo::<>([42]);
    // Specify everything
    foo::<i32, 0>([]);

...but you cannot:

    // Elide just const parameters    
    foo::<i32>([]);
    // Elide just type parameters
    foo::<0>([String::new()]);
error[E0107]: function takes 2 generic arguments but 1 generic argument was supplied
  --> src/main.rs:11:5
   |
11 |     foo::<i32>([]);
   |     ^^^   --- supplied 1 generic argument
   |     |
   |     expected 2 generic arguments
   |
note: function defined here, with 2 generic parameters: `T`, `U`
  --> src/main.rs:1:4
   |
 1 | fn foo<T, const U: usize>(_: [T; U]) {}
   |    ^^^ -  --------------
help: add missing generic argument
   |
11 |     foo::<i32, U>([]);
   |              +++

error[E0107]: function takes 2 generic arguments but 1 generic argument was supplied
  --> src/main.rs:13:5
   |
13 |     foo::<0>([String::new()]);
   |     ^^^   - supplied 1 generic argument
   |     |
   |     expected 2 generic arguments
   |
note: function defined here, with 2 generic parameters: `T`, `U`
  --> src/main.rs:1:4
   |
 1 | fn foo<T, const U: usize>(_: [T; U]) {}
   |    ^^^ -  --------------
help: add missing generic argument
   |
13 |     foo::<0, U>([String::new()]);
   |            +++

And similarly this is an error; you must annotate both lifetimes if you annotate one of them.

fn foo<'a: 'b, 'b>(_: &'b &'a str) {}
fn main() {
    foo::<'static>(&"");
}

For the lifetime parameters, you can

  • Elide all lifetimes
  • Annotate all lifetimes, if they are all early bound
  • (Annotate early bound lifetimes which are mixed late bound lifetimes, but get a future compatibility warning like was discussed above; I'm not going to dig into every nuance)

For the non-lifetime parameters, you can

  • Elide all parameters
  • Annotate all parameters
  • (On non-functions, annotate all parameters without defaults, and 0 of those with defaults from right to left; you can do this with types only on functions, and have to allow a deny-by-default future compatibility warning; I'm not going to dig into every nuance (but there are more details here))

You can choose a bullet out of each list independently.


The FLS document is talking about things like meeting bounds. It appears to be talking about more generic parameter positions than just functions, such as struct turbofish, impl headers, field definitions, and the like. They use the term "parameter initializers" for defaulted parameters. They say

A lifetime argument is conformant with a lifetime parameter when it outlives the lifetime specified by the lifetime parameter.

but there isn't always a lifetime specified by a parameter, and when there is, "outlives" is not always sufficient due to things like variance.[1]

They do talk about order sensibly...

All lifetime arguments come first, followed by constant arguments and type arguments in the order defined by the generic parameters, followed by binding arguments

...though binding arguments are only applicable in bounds and dyn types I believe. And context effects what is allowed to have defaulted parameters and how elision works.

Their description of what can be elided, e.g.

Any remaining generic parameters without corresponding conformant generic arguments are constant parameters with constant parameter initializers, lifetime parameters with either inferred lifetime arguments or elided lifetimes, type parameters with type parameter initializers or inferred type arguments,

...is incomplete as, like the demonstration earlier in this comment shows; once you annotate some lifetimes or parameters, you cannot always elide the rest. Or maybe it's implied by the section as a whole, and my legalese is not strong enough... as it is pretty heavy on the legalese.

So on the whole I don't think it's necessarily a great source of authority for this topic.


  1. And sometimes being outlived is sufficient due to contravariance. ↩︎

2 Likes

Ok. Is this how the rustc implements the turbofish resolution of generic arguments?

I'm not sure what that means.

I meant, does rustc implement pairing generic arguments and generic parameters in this way?