Can `T` when representing a borrow also hold a lifetime?

As a generic parameter T can represent any type. The type parameter T could represent a borrow (T: &U). For instance, binding T to u32 generates a concrete type (in this case value u32). Similarly, binding T to &u32 generates a concrete type (read-borrow of u32).

When lifetimes need to be made explicit, the generic parameter needs to bind both <lifetime, type> in order to generate a concrete type (given that Rust has two "kind types", type and lifetime).

When defining a generic type, will the compiler always reject the use of T when lifetimes need to be made explicit? If so, unless the lifetime and type parameters are qualifying different fields (not the case here) it seems that

// avoid when...
struct Something<'a, T>(&'a T)

should be avoided, and instead use:

// ... the following compiles
struct Something<T>(T)

Seems logical because the latter is just that much more generic allowing for: (i) T or &T and (ii) with and without explicit lifetimes.

Ok. Then what is going on here in what I'm about to describe? (see playground)

The following code compiles, and behaves as expected:

struct A { value: u32 } // move when copied
struct Something<T>(T);

fn go<'a, T>(input: &'a T, _: &T) -> Something<&'a T> {
   Something(input);
}

fn main() {
   let a1 = A { value: 3 };
   let a2 = A { value: 5 };   
   go(&a1, &a2);
  // returns Something(&A)
}

It returns Something(&A)

However, when I introduce a lifetime in the definition of the generic,

//..
struct Something<'a, T>(&'a T);

fn go<'a, T>(input: &'a T, _: &T) -> Something<'a, T> {
   Something(input);
}

fn main() {
  //..
  go(&a1, &a2);
  // returns Something(A)
}

... return a value Something(A); what happened to the symbols that indicate that this is a borrow? (a copy/move of the reference, not the value occurred here).

The scope of the difference seems limited to a type cast. But how do the symbols &A and A reconcile?

(The playground lays it out with print-outs of the types)

It returns a value with the type Something<'_, A>, notice the lifetime. This lifetime doesn't get printed out by std::any::type_name, but you can see it in compiler errors. For example if you replace print_type_name with

fn show_type(_: &(), tag: Option<&str>) {}

Then inspect the compiler errors, you'll see:

error[E0308]: mismatched types
  --> src/main.rs:21:14
   |
21 |    show_type(&result, Some("output"));
   |              ^^^^^^^ expected `()`, found struct `Something2`
   |
   = note: expected reference `&()`
              found reference `&Something2<'_, T>`

Note: &Something<'_, T> has a lifetime! This is what signifies that it holds a reference. playground

Perfect. Thank you. I need to find a way to print both the type and lifetime when they are specified in separate parameters (not something that is currently possible according to the docs for std::any::type_name). The irony: I did not see the error because it compiled :))

If the goal is to write a version of the code that "is as generic as possible/reasonable", am I right that specifying the generic definition with T is the way to go - even when you know that T is &U?

Essentially, is there a difference between Something<'a, T> and Something<T> when the two parameters are used to produce the same field/type (i.e., lifetime, type -> type, not lifetime -> type1 type -> type2)?

Yep, a function over fn foo<T>(value: T) {} can accept any value with one exception !Sized values like [_], str, or dyn Trait (note the lack of references). These have to be behind an indirection, like a &_, Box<_>, or Rc<_>.

However using a generic T that will only be instantiated to a single type does expose some implementation details, which may be undesirable

Humm. May you play that out a little more for me?

Sure, it doesn't exactly fit what you're trying to do here, but I thought I would mention it for future reference

struct SomePrivateType;
pub struct HoldsSomePrivateType(SomePrivateType);

pub struct GenericType<T>(T);
pub struct NotAsGeneric<'a>(&'a SomePrivateType);

// falls into the private_in_public error
pub fn doesnt_work(h: &HoldsSomePrivateType) ->  GenericType<&SomePrivateType> {
    GenericType(&h.0)
}

pub fn works(h: &HoldsSomePrivateType) ->  NotAsGeneric<'_> {
    NotAsGeneric(&h.0)
}

Yes, that's exactly correct. As you noticed in the initial post, Rust has two1 kinds: lifetimes, types. So Something<'a, T> has the kind (lifetime, type) -> type (which is isomorphic to lifetime -> type -> type).

1 and technically terms as well now, in a limited fashion with min_const_generics now stabilized

1 Like

I don’t understand this question. What exactly do you mean here? And in case it’s not obvious by that explanation, please include some explanation on why the following is the conclusion from a potential answer to that question

That's a good reminder; T is not "for all types" like I'm used to in Haskell where a type parameter a is any type. Rust has a min requirement of needing to know the size at compile time; DST after that where size matters again, but only at runtime for allocating memory on the heap (not references, but values). Is that right?

You can still use DSTs without allocating, for example:

let x: i32 = 0;
let x_dyn: &dyn std::fmt::Debug = &x;

It just requires indirection. In Haskell the indirection is implicit, in Rust it's explicit (because there are many different forms of indirection, each with their own trade offs).

Apologies for the confusion here. Restated, if I specify

struct Something<'a, T>(&'a T);

will the compiler reject Something(&T) even when the lifetime is elided or even Something(&'a T)?

I suspect the answer to both is yes, the compiler will now require two parameters when trying to build a concrete version of Something. Is that correct?

I guess you’ve meant to use <> instead of () here.. so you’re asking about Something<&T>, i.e. Something<'_, &T>? This type would then contain a &&T field.. is that what you’re asking about?

The types Something<'_, &T> and &&T are very comparable here. Both can work with elided lifetimes in the same kind of settings. Btw, leaving out the lifetime entirely, i.e. writing Something<'_, &T> instead of Something<&T> is discouraged and will (AFAIK) eventually be warned against, since it “hides” the fact that Something has a lifetime parameter.


Code example

struct Something<'a, T>(&'a T);

fn function<T>(x: Something<&T>) {}

(playground)
which is the same as

struct Something<'a, T>(&'a T);

fn function<T>(x: Something<'_, &T>) {}
or ....

or

struct Something<'a, T>(&'a T);

fn function<'a, T>(x: Something<'a, &T>) {}

or

struct Something<'a, T>(&'a T);

fn function<'b, T>(x: Something<'_, &'b T>) {}

or

struct Something<'a, T>(&'a T);

fn function<'a, 'b, T>(x: Something<'a, &'b T>) {}

or

struct Something<'a, T>(&'a T);

fn function<'a, 'b: 'a, T: 'b>(x: Something<'a, &'b T>) {}
1 Like

Exactly. I can use DSTs behind something Rust can size at compile time. Perfect, thank you.

You can enforce this with #[deny(elided_lifetimes_in_paths)]

2 Likes

Thank you for the code snippets. That brings out an interesting point because while the inputs as you have them, compile, the compiler rejects the following:

struct Something<'a, T>(&'a T);

fn go<'a, T>(input: &'a T, _: &T) -> Something<&'a T> {
   Something(input);
}

but accepts it when I change the return type of fn go to Something<'a, T>.

I will have to re-read the posts with a fresh eye tomorrow, but clearly the compiler binds T to &'a T but not <'a, T> to &'a T when building types.

Well, this function has multiple problems. Ignoring the semicolon, the first thing to learn is that Something<&'a T> stands for Something<'_, &'a T>. They are meaning entirely the same. This means that Something<&'a T>, i.e. Something<'_, &'a T> contains, by definition, a &'_ &'a T field. You’re passing a &'a T here, so that’s problematic. Fixing this problem, we get

fn go<'a, T>(input: &'_ &'a T, _: &T) -> Something<'_, &'a T> {
   Something(input)
}

this still doesn’t work because lifetime elision does not support having more than one lifetime on the left hand side of the function (with some exception for methods, i.e. when self parameters are involved). There’s actually three lifetimes now, previously there still were two lifetimes. And the (previously invisible) '_ is an explicit elided lifetime. So to get everything work, we do indeed need explicit lifetimes, e.g.

fn go<'a, 'b, T>(input: &'b &'a T, _: &T) -> Something<'b, &'a T> {
   Something(input)
}

But the situation is the same if you used a

struct SomethingElse<T>(T);

fn go<'a, T>(input: &&'a T, _: &T) -> SomethingElse<&&'a T> {
   SomethingElse(input)
}

the same kind of error, the same kind of solution, no difference :wink:

Thank you @steffahn. Does what you are saying mean I should be able to use

struct SomethingElse<T>(T);

and

struct Something<'a, T>(&'a T);

as "one in the same"? A quick yes, no is all I need if the answer is clear to you in what you have already said. I will review in detail tomorrow.

I’ll give you a qualified yes. If you have these two types, then you can use Something<'a, T> or SomethingElse<&'a T> in the same way, yes. And the elided SomethingElse<&T> corresponds to Something<'_, T>.

Just, as @RustyYato already said, the SomethingElse type does not “hide” the fact that it contains a reference if it contains a reference. So you won’t be able to refactor it to use something other than &T internally later (like – IDK – maybe raw pointers or whatever). And obviously it allows for writing SomethingElse<S> for non-reference types S, too. Users of the type might be confused why it only ever appears as SomethingElse<&'a T> in your API.

1 Like

Articulating the correspondence between the two type declarations is helpful. Here is an expansion of that approach:

parameter A<T>(T) type B<'a,T>(&'a T) type
value: T A<T> fails to compile
&value A<&T> B<'_, T>
&'a value A<&'a T> B<'a, T>

What type-level bindings work with the compiler? For instance, can I use <'a, T> to describe <&'a T> in the return type of a function?

type declaration return type compiles? note
X<T> X<&'a T> yes T encodes both the type and named borrow
X<T> X<'a,T> no error: unexpected lifetime parameter
X<'a,T> X<&'a T> no error: missing lifetime parameter

playground

So all in all, to your "qualified yes", while the first table describes a correspondence, the compiler treats them as separate types. Any attempt to treat <'a, T> as <T> (and vice versa), even when T: &U, fails to compile.

For me the take-away is that using <'a, T> in a type declaration seems to make sense only when the parameters are required to specify more than one type in the "thing" being typed. e.g.,

struct DoItHere<'a, T> {
    value: T, // concrete type when provided with a type parameter 
    resource: &'a Resource, // concrete type when provided with a lifetime parameter
}
struct NotHere<'a, T> {
   value: &'a T // avoid, use T where T can be &U when needed, with named lifetimes when needed
}

As someone trying to scope out the intent of the <'a, T> syntax, the value and limits of the expression are more explicit when doing something like <'data, 'connection, User> where the grammar requires lifetimes be first, then types, but does not imply a relationship between them. The "newtype" example I was using seems to defeat, if not miss altogether, the value of what can be expressed using this type constructing syntax.

Finally, while I can perhaps see why, it is somewhat counter-intuitive that T that can host/encode a borrow with a named lifetime, cannot be cast to <'a, T>. The information is there, why not then bind accordingly? I think that's where I started to try and understand the distinction; difference with distinction? or six/half-dozen? In practice, we concluded difference with distinction. End of what is needed to know to use the syntax.

However, curiosity killing the cat: could that information consistently inform the lifetimes of the subtypes without creating ambiguity in the type system? (i.e., is this rejected code that could be admitted) What prevents the compiler from accepting an elided lifetime when an explicit one is specified by the type definition? I get that there is no obvious reason the elided lifetime should be a valid interpretation for what the subtype requires, but how might it be wrong? Again, I can see how the lifetime of the "thing" as a whole is unrelated to the lifetime of a field in the "thing", but when "correct" a relationship does exist: the lifetime of the field borrow must be at least as long (if not longer) than that of the "thing"... The articulation of the inherent relationships might draw a line between shared scope and the limits of what the lifetime of the whole says about that of it's fields in a way that informs more soundness, more streamlined thinking, into our designs.

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.