Trait upcasting and borrowed data escape error

I am not good at English. Sorry if there are any funny expressions.

I mistakenly coded the upcast from &dyn SomeTrait to &dyn Any as follows.
Of course, it was an error, but when I think about it, I don't quite understand why it was an error.
It seems certain that the static lifetime bound of Any has something to do with it...
It is confusing when I think about the difference between a non-error point and an error point.

Any advice would be appreciated.

use std::any::Any;

// -- AsAny -- //

trait AsAny {
    fn as_any(&self) -> &dyn Any;
}

impl<T: Any> AsAny for T {
    fn as_any(&self) -> &dyn Any {
        self
    }
}

// -- SomeTrait -- //
// πŸ™„ Instead of Any, write AsAny to build.
trait SomeTrait: Any {
    fn test(&self);
}

// -- Test code. -- //

fn main() {
    let some = &0 as &dyn SomeTrait;
    // βœ… OK.
    let _any = some.as_any();
}

fn _test1(arg: &dyn SomeTrait) {
    // ❌ Error (Borrowed data escapes...).
    let _any = arg.as_any();
}

impl<'a, T: Any> SomeTrait for T {
    fn test(&self) {
        // βœ… OK.
        let _any = self.as_any();
    }
}
error[E0521]: borrowed data escapes outside of function
  --> src\main.rs:31:16
   |
29 | fn _test1(arg: &dyn SomeTrait) {
   |           ---  - let's call the lifetime of this reference `'1`
   |           |
   |           `arg` declared here, outside of the function body
   |           `arg` is a reference that is only valid in the function body
30 |     // ❌ Error.
31 |     let _any = arg.as_any();
   |                ^^^^^^^^^^^^
   |                |
   |                `arg` escapes the function body here
   |                argument requires that `'1` must outlive `'static`
fn _test1(arg: &dyn SomeTrait) {
    // ❌ Error (Borrowed data escapes...).
    let _any = arg.as_any();
}

To see what's happening, let's expand impl AsAny. T is &dyn SomeTrait.

impl AsAny for &dyn SomeTrait {
    fn as_any(self: & &dyn SomeTrait) -> &dyn Any {
        self
    }
}

So far so good, but we left out the lifetimes.

impl AsAny for &'a dyn SomeTrait {
    fn as_any(self: & &'a dyn SomeTrait) -> &dyn Any {
        self
    }
}

Wait, something's wrong. We pulled 'a out from nowhere. We'd need impl<'a>, but we don't have any lifetime arguments on the original impl. The fix, which is what I think* the compiler does:

impl AsAny for &'static dyn SomeTrait {
    fn as_any(self: & &'static dyn SomeTrait) -> &dyn Any {
        self
    }
}

*: I'm uncertain about this; I may have taken a bad turn. e.g. lifetimes are part of types, and template type args can match types with lifetimes, but that doesn't seem to be happening here.

2 Likes

That's not quite the correct reasoning ^^

A lifetime can very well "hide" inside of a generic like T and a priori the implementation is fully generic over 'a, but...

...then there's the T: Any bound on the impl and trait Any: 'static is a supertrait bound (as well as the generic implementation for Any requiring T: 'static) means that we're limited to the case &'a dyn SomeTrait: 'static. In case you aren't fully familiar with T: 'static bounds, in this case this directly translates to 'a: 'static (which is only true if 'a itself is 'static)

6 Likes

To address the original question, the important detail here is that the method call .as_any() can implicitly introduce a new level of reference (sometimes called "autoref") so that it creates a &&dyn SomeTrait which has an as_any method by means of using the implementation of AsAny for T with T itself being a reference type &dyn SomeTrait (then &self has the type &T being &&dyn SomeTrait)

That's most likely not what you wanted, anyways. You wanted to turn &dyn SomeTrait into &dyn Any without introducing another level of borrowing and indirection. But with the : Any supertrait bound, unlike with a : AsAny one, the type dyn SomeTrait does not have an as_any method, because the generic implementation for all types T: Any only applies to types who are both T: 'static and T: Sized, the latter (the type being of statically known size) is never fulfilled by the type dyn SomeTrait. Only because of the method not being present for dyn SomeTrait, the compiler decided to try automatically introducing an additional layer do referencing.

The situation in the last example, the impl<'a, T: Any> SomeTrait for T is different in that here T: Sized (which is almost a bound for all generic type arguments unless you opt out via T: ?Sized) is fulfilled, as is T: 'static (or in this case T: Any is written directly with the same effect).

The introduction of a second level of reference means that there are restrictions on the lifetimes of the inner reference, which the previous two replies discuss. This restriction then leads to the second example failing to compile with an error about lifetimes. The first code example is similar, it does compile, but it only compiles due to static promotion. The expression &0 in Rust is allowed to be of type &'static i32, comparable to how string literals are of type &'static str, pointing to data that exists, hardcoded into the binary, for the entire duration of the program. If you used a local instead like let n = 0; and let some = &n as &dyn SomeTrait then you'd get a compilation error complaining about lifetimes for that code, too.

5 Likes

The 'a in &'a dyn SomeTrait: 'static is not implicitly bound by 'a: 'static. The 'static bound on the trait takes precedence for the default trait object lifetime, making it independent of the reference lifetime. If the reference lifetime is forced to be 'static, the example compiles.

(Perhaps you accidentally overloaded your usage of 'a.)


trait SomeTrait: Any {
    fn test(&self);
}

fn _test1(arg: &dyn SomeTrait) {
    // ❌ Error (Borrowed data escapes...).
    let _any = arg.as_any();
}

This version tries to call <&'_ (dyn SomeTrait + 'static) as AsAny>::as_any(&arg) and fails because the lifetime of the reference is not 'static. As mentioned above, it works if you change the reference lifetime to be 'static.

There's no implementation of AsAny for dyn SomeTrait here. The blanket implementation has an implicit Sized bound, and there's nothing making the compiler supply an implementation, unlike the case below.

If you thought there was an implementation for dyn SomeTrait, compare and contrast with this error.

    // Different error because `dyn SomeTrait` is not `Sized`
    let _any = <dyn SomeTrait as AsAny>::as_any(arg);

// Edited to have an `AsAny` bound
trait SomeTrait: AsAny {
    fn test(&self);
}

fn _test1(arg: &dyn SomeTrait) {
    let _any = arg.as_any();
}

This calls <dyn SomeTrait + 'static as AsAny>::as_any(arg) and succeeds. The compiler supplies this implementation because of the supertrait bound (trait SomeTrait: AsAny).

1 Like

Oh... "autoref".

The automatic trials by "autoref" and "autoderef" sometimes select different option than I expect, And then outputs an unexpected error for me. This is a problem for beginners…. I thought I was slightly familiar with it these days, but I'm not quite there yet.

Thank you!!

I wondered what happened when I saw "& &"....
I see, in this way I made a big mistake....
Thank you very much.

I often had trouble with "autoref" and "autoderef."

But as you have shown me, changing the notation of the method makes it clearer.

  • <&'_ (dyn SomeTrait + 'static) as AsAny>::as_any(&arg)
  • <dyn SomeTrait as AsAny>::as_any(arg)

Thanks for the good tips.

autoref and autoderef save a lot of typing *** and &&&, but oh-boy do they cause confusion at times...

I didn't mean to talk about the trait object lifetime at all; I know it exists, but it didn't seem relevant for the explanation. (And indeed it isn't relevant at all: as you correctly deduce yourself, it's always 'static.) I only meant to talk about the lifetime of the refetence, nothing else. The T: Any bound I was talking about is the bound on the impl<T: Any> AsAny for T implementation, and I was correcting @tbfleming's guess as to why this implementation only covers the case T == &'static dyn SomeTrait and not T == &'a dyn SomeTrait for any other lifetime 'a. The observation @tbfleming gave was correct, but the reason why (which they guessed and explicitly doubted themself) wasn't correct.

1 Like

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.