4
votes

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
for<'a> doesn't apply here because that's universal quantification ("for all 'a") but you are asking for existential quantification ("for some 'a (to be determined by T)"). The problem with that is the existential version is unsound, because a temporary reference inside do_something_ref cannot be proven to outlive "some 'a".trentcl
@trentcl Ah the fact that it works on type parameters (and not on associated types) is insightfulMingwei Samuel
Argument types of Fn traits are input (parameter) types, not output (associated) types, so a rough equivalent to Fn(&'a X) would be Pipe<&'a usize>, like in my first link. (Fn(&'a X) is essentially sugar for not-yet-stabilized Fn<(&'a X,), Output = ()>; Output is an associated type but Args is a parameter)trentcl
the fact that it works on type parameters (and not on associated types) is insightful - It is my understanding that making the trait generic over Item effectively creates a new trait for each different Item. With lifetimes this gives the compiler more freedom in determining what the particular trait is allowed to do. As I understand it, this is why adding an explicit generic lifetime to the trait helps when the trait has an output type.user4815162342

3 Answers

4
votes

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.)

0
votes

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);
}
0
votes

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)
    }
}