Use ops::Index inside trait without making trait generic

I've been stuck on this for a while now..
I'm trying to make something like this work without adding generics to IndexAsDebug.
However I get the parameter type `O` may not live long enough.

trait IndexAsDebug {
    fn index_as_debug(&self, index: usize) -> &dyn Debug;
}
impl<T, O> IndexAsDebug for T
where
    T: Index<usize, Output = O>,
    O: Debug,
{
    fn index_as_debug(&self, index: usize) -> &dyn Debug {
        self.index(index)
    }
}

This seems to work but I'd like to avoid the generic lifetime inside Workaround<'a> and I don't see why it would be necessary.

trait Workaround<'a> {
    fn index_as_debug(&'a self, index: usize) -> &dyn Debug;
}
impl<'a, T, O> Workaround<'a> for T
where
    T: Index<usize, Output = O>,
    O: Debug + 'a,
{
    fn index_as_debug(&'a self, index: usize) -> &dyn Debug {
        self.index(index)
    }
}

Could someone help me solve this or help me understand why the first version does not work?

Playground link:

Previous discussion:

So let me first explain the problem here and also why the workaround works. The relevant section in the reference is this one

What you have is a type O: Debug. You want to convert it into a dyn Debug. The problem is that trait objects have a lifetime. If you don’t specify that, it is added by the elision rules I linked. A dyn Debug + 'a can only be made from O: Debug when O: 'a is also met.

A common case would be when the trait object has a 'static bound like e.g. in the case of using Box<dyn Debug> on its own desugars to Box<dyn Debug + 'static>. When O = &'some_lifetime SomeType is a reference type that reference type is not valid for 'static lifetime so it won’t life long enough to allow conversion of Box<O> to Box<dyn 'static>.

In your case, the act of returning a &dyn Debug make the compiler add the lifetime constraint &'a (dyn Debug + 'a)

The original code desugars to

use std::ops::Index;
use std::fmt::Debug;
trait IndexAsDebug {
    fn index_as_debug(&self, index: usize) -> &dyn Debug;
}
impl<T, O> IndexAsDebug for T
where
    T: Index<usize, Output = O>,
    O: Debug,
{
    fn index_as_debug<'a>(&'a self, index: usize) -> &'a (dyn Debug + 'a) {
        self.index(index)
    }
}

and your workaround to

use std::ops::Index;
use std::fmt::Debug;
trait Workaround<'a> {
    fn index_as_debug(&'a self, index: usize) -> &'a (dyn Debug + 'a);
}
impl<'a, T, O> Workaround<'a> for T
where
    T: Index<usize, Output = O>,
    O: Debug + 'a,
{
    fn index_as_debug(&'a self, index: usize) -> &'a (dyn Debug + 'a) {
        self.index(index)
    }
}

The important detail that makes the workaround work is that there is a O: Debug + 'a constraint that fits the dyn Debug + 'a. You can however make a simpler workaround, there’s actually two ways I can think of. The easiest way — and that is something that one should always consider trying on an error message the parameter type O may not live long enough, is to add + 'static to the bounds of O, as in:

use std::ops::Index;
use std::fmt::Debug;
trait IndexAsDebug {
    fn index_as_debug(&self, index: usize) -> &dyn Debug;
}
impl<T, O> IndexAsDebug for T
where
    T: Index<usize, Output = O>,
    O: Debug + 'static,
{
    fn index_as_debug(&self, index: usize) -> &dyn Debug {
        self.index(index)
    }
}

which already compiles. In this case, the inferred &'a (dyn Debug + 'a) return value could be improved into a more general &'a (dyn Debug + 'static)

use std::ops::Index;
use std::fmt::Debug;
trait IndexAsDebug {
    fn index_as_debug(&self, index: usize) -> &(dyn Debug + 'static);
}
impl<T, O> IndexAsDebug for T
where
    T: Index<usize, Output = O>,
    O: Debug + 'static,
{
    fn index_as_debug(&self, index: usize) -> &(dyn Debug + 'static) {
        self.index(index)
    }
}

fn foo<'a>(x: &'a (dyn Debug + 'static)) {}
fn bar(x: &dyn Debug) {}

fn test<T: IndexAsDebug>() {
    let x: T = (||unimplemented!())();
    bar(x.index_as_debug(0));
    
    foo(x.index_as_debug(0)); // this won’t work if the 'static is removed
                              // in the return value of index_as_debug
}

Of course there’s downsides to a 'static constraint


use std::ops::Index;
use std::fmt::Debug;
trait IndexAsDebug {
    fn index_as_debug(&self, index: usize) -> &(dyn Debug + 'static);
}
impl<T, O> IndexAsDebug for T
where
    T: Index<usize, Output = O>,
    O: Debug + 'static,
{
    fn index_as_debug(&self, index: usize) -> &(dyn Debug + 'static) {
        self.index(index)
    }
}

fn test1() {
    vec![1,2,3].index_as_debug(0);
    let x = 1;
    // vec![&x].index_as_debug(0); // does not work
}

A way to improve here, going back to &dyn Debug would be as follows:

use std::ops::Index;
use std::fmt::Debug;
trait IndexAsDebug {
    fn index_as_debug(&self, index: usize) -> &dyn Debug;
}
impl<T> IndexAsDebug for T
where
    T: Index<usize>,
    T::Output: Sized + Debug,
{
    fn index_as_debug(&self, index: usize) -> &dyn Debug {
        self.index(index)
    }
}

fn test1() {
    vec![1,2,3].index_as_debug(0);
    let x = 1;
    vec![&x].index_as_debug(0); // does work
}

...coming up with this last code took me ages. For some reason rustc is only happy with it if the extra parameter O goes away. There seems to be a rule that T::Output type has a lifetime at least as long as T itself which makes this valid in the first place since our lifetime to be met is the lifetime of a reference to T.

1 Like

I filed a bug report about this a few months ago, but it hasn’t gotten any traction:

As best as I can tell, if the Index trait is implemented, you can unconditionally get an &'a Index::Output from &'a Self via index(), so no additional lifetime annotations are needed (and Index::Output: 'a). At the moment, that inferred lifetime bound doesn’t get applied to a named type parameter that’s equal to Index::Output for some reason.

As far as I can tell, the HRTB syntax can’t express this either; you’d need one of these bounds (all equivalent, none legal Rust):

  • for<'a where T:'a> O:'a
  • for<'a> O:'a || 'a:T
  • for<'a> !('a:O && T:'a)
  • !exists<'a> 'a:O, T:'a
  • exists<'a> O:'a, 'a:T

This last one is closest to valid (by introducing a dummy lifetime parameter 'a), but types aren’t allowed on the right side of an “outlives” relationship.

2 Likes

Thanks a lot for the detailed responses.
Things are definitely more clear now and using T::Output: Sized + Debug solves my problem.

@2e71828 the issue you linked explains my confusion. Feel free to add this example if you see value in doing so.