So let me first explain the problem here and also why the workaround works. The relevant section in the reference is this one
https://doc.rust-lang.org/stable/reference/lifetime-elision.html#default-trait-object-lifetimes
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.