'temporary value dropped while borrowed' on value that is not borrowed

I'm getting the error temporary value dropped while borrowed from this code

use std::hint::black_box;

// The error only occurs if the type in `Wrapper` is 'complex' (`<&() as Trait>::Type` rather than `&()`)
pub trait Trait {
	type Type;
}
impl Trait for &() {
	type Type = Self;
}

// The error only occurs if `Wrapper` uses a lifetime
struct Wrapper<'a>(<&'a () as Trait>::Type);

fn foo<T>(_: T, _: &T) {}

fn main() {
	let data = Wrapper(&());

	// The error only occurs if the temporary is the result of an expression (not a literal)
	foo(Wrapper(&black_box(())), &data);

	data;
}

error[E0716]: temporary value dropped while borrowed
  --> src/main.rs:17:15
   |
17 |     foo(Wrapper(&black_box(())), &data);
   |                  ^^^^^^^^^^^^^         - temporary value is freed at the end of this statement
   |                  |
   |                  creates a temporary value which is freed while still in use
18 |
19 |     data;
   |     ---- borrow later used here
   |
help: consider using a `let` binding to create a longer lived value
   |
17 ~     let binding = black_box(());
18 ~  foo(Wrapper(&binding), &data);

The error seems to imply that data is borrowing from the temporary value passed to foo, but I don't see why.

Is there any way to tell the compiler that data doesn't borrow from the other argument of foo? I've noticed that if I make foo not generic, and give each parameter a different lifetime, the error goes away, so maybe it's just beause the parameters share the same lifetime. If that's the case, is there any way for a function to take two of the same type, just with different lifetimes?

I know I could just follow the suggestion in the error message, but this is in the public API for a library I'm writing, and it would be nicer if users had the option of writing their inputs inline, e.g.

create_bind_group(
	Material {
		base_colour_texture: Some(&texture.create_view()),
		normal_map: Some(&normal_map.create_view()),
		..Material::default()
	},
	Layout::<Material>::new()
);

vs

let texture_view = texture.create_view();
let normal_map_view = normal_map.create_view();
create_bind_group(
	Material {
		base_colour_texture: Some(&texture_view),
		normal_map: Some(&normal_map_view),
		..Material::default()
	},
	Layout::<Material>::new()
);

The important lesser-known fact here is that traits are always invariant over their generic parameters (and Self counts just like a generic parameter) — lifetimes in those generics cannot be transparently lengthened or shortened, like they can with ordinary uses of references. This is because traits don’t define whether a generic parameter can be covariant (shortenable) or contravariant (lengthenable), because different implementations could use the generic parameter in different ways. For example:

struct Foo;
impl<'a> Trait for &'a Foo {
	type Type = &'a str; // it'd be okay if `'a` could be covariant...
}

struct Bar;
impl<'a> Trait for &'a Bar {
	type Type = fn(&'a str); // ...but for this impl it could only be contravariant.
}

Then, since Wrapper makes use of the trait’s associated type, Wrapper<'a> itself is invariant in 'a. Therefore we end up with the following:

  • You call foo::<T>() on two values, one of which is data.
  • What is T? it must be Wrapper(&'x ()), with some particular lifetime 'x.
  • What is 'x? It must be a lifetime compatible with borrowing data and with borrowing the temporary value returned from black_box(()).
  • Therefore, 'x must end when the temporary value is dropped, at the end of the foo(...).
  • Therefore, all Wrapper(&'x ())es, including data, are invalid when foo is dropped.

What the best solution to this problem is depends on what you’re actually wanting Trait and Wrapper to do outside of a tiny example. Some solutions include:

  • Define Trait so it is implemented for the referent instead of the reference — keeping the references outside of the trait and thus still covariant.
  • Define Trait so it is implemented for a placeholder type that doesn’t involve the lifetime you want to keep covariant, such as &'static (). This usually means making the associated type generic over a lifetime, too.
  • Add a “manual reborrow” method to Trait to manually obtain the flexibility that you would get through covariance.
6 Likes

Thank you for the explaination.

In this case, Trait is something based on the condtype crate:

trait CondType<const B: bool> {
	type Type;
}
impl<const B: bool, T> CondType<B> for T {
	default type Type = ();
}
impl<T> CondType<true> for T {
	type Type = T;
}

type If<const B: bool, T> = <T as CondType<B>>::Type;

Which I use to pick a type based on a const-generic bool (i.e. If<true, T> == T and If<false, T> == ()).

Good idea, I decided to move the reference out of the If (If<B, &T> -> &If<B, T>) and that fixes the example I started with, but in my full code I found a potential issue with the generic_const_exprs feature (although it's incomplete so I can't really complain)

#![feature(generic_const_exprs)]

use std::hint::black_box;
use std::marker::PhantomData;

struct Material<'a>(&'a ());

struct Inner<const TYPE: u8>;

struct Wrapper<T>(Inner<{ 0 + 0 }>, PhantomData<T>);

fn foo<T>(_: T, _: &Wrapper<T>) {}

fn main() {
	let wrapper = Wrapper(Inner, PhantomData);

	foo(Material(&black_box(())), &wrapper);

	wrapper;
}

This code complains with the same error, but if I remove the generic const expression (change { 0 + 0 } to { 0 }), it compiles fine. Is this intentional or should I report this?