How can I constrain an associated type to accept any reference lifetime?

Viewed 849

I have a simple trait which has a function which accepts an associate type.

pub trait Pipe {
    type Item;
    
    fn push(&mut self, value: Self::Item);
}

Now I want to do something with a Pipe that accepts references rather than owned values.

pub fn do_something_ref<T>(trait_inst: T)
where
    T: Pipe<Item = &usize>
{
    let val: usize = 5;
    trait_inst.push(&val);
}

The references will not live longer than the caller context lifetime. Maybe the Pipe clones the value. Maybe it will just println it. But it will not be allowed to store it since that would violate the caller context lifetime.

However the code above give an error: error[E0637]: `&` without an explicit lifetime name cannot be used here.

Playground Link

How can I make this work?

Here's my attempt.

So, it seems that higher ranked trait bounds (HRTB) are useful here. If I can say "P: Pipe<Item = &'a> for all possible 'a" then that would mean the Pipe would accept very short-lived references.

pub fn do_something_ref<T>(trait_inst: T)
where
    for <'a> T: Pipe<Item = &'a usize>
{
    let val: usize = 5;
    trait_inst.push(&val);
}

I think this should work, but it doesn't compile either: error[E0582]: binding for associated type `Item` references lifetime `'a`, which does not appear in the trait input types.

Playground Link

Is that error a bug?

Could be https://github.com/rust-lang/rust/issues/49601 ?

So it seems this error only happens because the 'a isn't in a spot that the compiler recognizes as valid for for<'a>. So I tried to find some workaround by adding an unused lifetime parameter or helper traits, and although I found some solutions which initially compiled, none actually work when you try to use them.

How can I get this to work how I want? Thanks!


Old discussion on internals: "Opposite of &’static"

3 Answers

If not holding on to the reference is a property of push(), you can fix the issue by modifying the trait (the question is not clear on whether that's allowed):

pub trait Pipe<'a> {
    type Item;

    fn push(&mut self, value: Self::Item);
}

pub fn do_something_ref<T>(mut trait_inst: T)
where
    T: for<'a> Pipe<'a, Item = &'a usize>,
{
    let val: usize = 5;
    trait_inst.push(&val);
}

...and it can be implemented (playground).

Without modifying the trait there would, as the other answer puts it, be nothing stopping push() from holding onto the value. (A similar concern prevents streaming iterators from working - except you cannot "fix" iterators because the fix would stop regular iterators from working.)

You can do that adding a generic lifetime to your function:

pub fn do_something_ref<'i, T>(mut trait_inst: T)
where
    T: Pipe<Item = &'i usize>
{
    let val: usize = 5;
    trait_inst.push(&val);
}

However, this will fail with the error message:

error[E0597]: `val` does not live long enough
  --> src/lib.rs:12:21
   |
7  | pub fn do_something_ref<'i, T>(mut trait_inst: T)
   |                         -- lifetime `'i` defined here
...
12 |     trait_inst.push(&val);
   |     ----------------^^^^-
   |     |               |
   |     |               borrowed value does not live long enough
   |     argument requires that `val` is borrowed for `'i`
13 | }
   | - `val` dropped here while still borrowed

Which is expected. What prevents your Pipe implementation to store that reference and use it after returning from do_something_ref, causing undefined behaviour? Rust prevents you from doing that by requiring that the 'i lifetime comes from outside this function. Something like this does work:

pub fn do_something_ref<'i, T>(mut trait_inst: T, x: &'i usize)
where
    T: Pipe<Item = &'i usize>
{
    trait_inst.push(&x);
}

Another option is to ... give up on generics and just create two traits.

This is effectively trentcl's comment "lift the reference so the lifetime becomes part of the signature of push", and is what I ended up doing.

pub trait Pipe {
    type Item;
    
    fn push(&mut self, value: Self::Item);
}
pub trait RefPipe {
    type Item;
    
    fn push(&mut self, value: &Self::Item); // Reference here
}

(And RefPipe can be treated as Pipes and received owned values, if you want)

impl<P: RefPipe> Pipe for P {
    type Item = P::Item;

    fn push(&mut self, item: Self::Item) -> Result<(), &'static str> {
        Pipe::push(self, &item)
    }
}
Related