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.

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.