Why can't I use reference of a reference in the current scope? (cast requires that `'1` must outlive `'static`)

For a hierarchical cache that has to be generic to support both Send + Sync and also not allow it based on the generic, I need to temporarily use a ref of a ref. The original generic motivation and idea can be seen in this question.

The problem is that I need to cast Hierarchical to dyn View and use it as parent (I can't use Hierarchical as is because it'll lead to a nesting spree and compilation problems, but this seems to be a secondary problem compared to the lifetime issue I'll mention next). But this doesn't work because of lifetime issues. What I don't understand is that I'm using a reference of a reference only until the end of the function. Why does Rust prevent me from using that reference?

The minimal example of the problem can be found here.

I would appreciate your help here.

1 Like

I think the 'static requirement is coming from your impl on Deref

impl<T> View for T
where
    T: Deref<Target = dyn View>,
{
    fn get_it(&self) -> String {
        (**self).get_it()
    }
}

The dyn View there isn't behind a reference so it's actually dyn View + 'static

Adding a lifetime there appears to fix the problem
Playground

impl<'a, T> View for T
where
    T: Deref<Target = dyn View + 'a>,
{
    fn get_it(&self) -> String {
        (**self).get_it()
    }
}

If you only had that impl to work around the issue of Sized bounds in ViewCow, you might be to adopt a pattern similar to ToOwned so you can have a ?Sized type behind the reference and an owned Sized type.

2 Likes

Damn! You found it! Thanks a lot. Never occurred to me to look there. Thanks a lot!

But I have to ask, how did you figure it out? I've been staring at the code for hours and couldn't find it, and the error message wasn't really helpful to make me find it.

Can you elaborate on what you mean with the pattern similar to ToOwned? I'm sure you already know (as I mentioned it in a comment) that I can't use ToOwned. Please feel free to answer on that question too. These patterns are gold and they're what make us good devs :slight_smile:

2 Likes

When trait objects are involved, the error mentions 'static, and there's no obvious place a static bound appears to be introduced it's very often because there's a bare dyn Trait somewhere that needs an explicit lifetime constraint. I agree the error messages could probably be more helpful in these cases, but I imagine it would be hard to filter out false positives on a diagnostic like that.


Basically something like this where you have a separate type for the borrowed and owned cases[1]

pub trait View {
    type Owned: View<Owned = Self::Owned>;

    fn get_it(&self) -> String;
}

Then you can make the type parameter on ViewCow ?Sized

pub enum ViewCow<'a, P: View + ?Sized + 'a> {
    Borrowed(&'a P),
    Owned(P::Owned),
}

which allows the code to compile without that blanket impl of View for Deref types, by just passing the &dyn View directly instead of an &&dyn View
Playground

pub struct TheObject {
    s: String,
}

pub trait View {
    type Owned: View<Owned = Self::Owned>;

    fn get_it(&self) -> String;
}

impl View for TheObject {
    type Owned = TheObject;
    fn get_it(&self) -> String {
        self.s.clone()
    }
}

/// Notice that View doesn't implement ToOwned in the real application as it's a database ref, so we cannot use std::borrow::Cow
pub enum ViewCow<'a, P: View + ?Sized + 'a> {
    Borrowed(&'a P),
    Owned(P::Owned),
}

impl<'a, P: View + 'a> View for ViewCow<'a, P> {
    type Owned = P::Owned;

    fn get_it(&self) -> String {
        self.as_bounded_ref().get_it()
    }
}

impl<'a, P: View + 'a> ViewCow<'a, P> {
    pub fn as_bounded_ref(&self) -> &dyn View<Owned = P::Owned> {
        match self {
            ViewCow::Borrowed(r) => *r,
            ViewCow::Owned(o) => o,
        }
    }
}

pub struct Hierarchical<'a, P: View + ?Sized> {
    parent: ViewCow<'a, P>,
    new_value: Option<String>,
}

impl<'a, P: View + 'a> View for Hierarchical<'a, P> {
    type Owned = P::Owned;

    fn get_it(&self) -> String {
        match &self.new_value {
            Some(v) => v.to_string(),
            None => self.parent.get_it(),
        }
    }
}

fn do_something<P: View>(parent: &Hierarchical<P>) {
    let h: &dyn View<Owned = P::Owned> = parent;
    let child = Hierarchical {
        parent: ViewCow::Borrowed(h),
        new_value: None,
    };
    // it's OK to drop child here...
}

fn main() {
    {
        let obj = TheObject {
            s: "Hello first".to_string(),
        };

        let hierarchical = Hierarchical {
            parent: ViewCow::Borrowed(&obj),
            new_value: None,
        };

        println!("{}", hierarchical.get_it());
    }
}

That style may not work in your actual code for a variety of reasons[2], but it would let you avoid the extra level of indirection.

There may also be a better way to structure this alternative, I haven't thought about it a lot.


  1. I'm a little surprised type Owned: View<Owned = Self::Owned> doesn't produce an error about cycles during type checking ↩︎

  2. or may just get too complicated to want to deal with since you have another type flying around due to the associated type ↩︎

Thanks a lot for the explanation! Cheers!

I wonder if we may collect Rust's version of fractal. These little inconsistencies (when type is usually considred Sized, not not always, when “normal” types are assumed to support arbitrary lifetimes but dyn types don't, when adding mut does something really crazy.

Most of these are much less benigh that PHP's comparion of strings (where string "1e3" is considered equal to string "1000") and other such warts, but I wonder what's the size of that fractal currently.

P.S. I'm not saying that Rust is awful and we should stop using it because of these warts. In fact I don't think any popular language may exist without something like that: bad decision happen and if you don't realize they are problematic in time you can not change them later, because… compatibility. You can't expect to break program with changes in your languages every couple of weeks and still stay popular. But still… collection of these gotchase may be a nice blog post (or wiki) and also useful help page.

1 Like

No, these aren't inconsistencies or warts.

  • Generic bounds on type parameters assuming Sized and traits not assuming Self: Sized was a very conscious decision, because these defaults are the right ones. They remove a huge amount of noise, since a value typed with a generic parameter must usually be sized in order to be passed around by-value, while this isn't empirically the case with trait declarations.
  • dyn Trait performs type erasure, therefore it must have an explicit lifetime – the compiler would stand no chance getting it from anywhere else, since lifetimes are intrinsic to a concrete type, and that's exactly what is unknown about a value underlying a trait object.

The mut example is the only one that may be legitimately surprising to someone new to the language, but even that one isn't an inconsistency. On the contrary – it's the result of how patterns and default binding modes compose. (And mind you, it wouldn't exist if default binding modes weren't a thing – that feature was pushed through aggressively by the language team, and apparently nobody tried this particular combination before stabilizing the rules. It's still not inconsistent, it's just bad.)

1 Like

The rules for string comparisons in PHP also weren't just random. Someone thought it's a good choice. And someone, probably, still thinks it's good decision.

It may reduce amount of typing, but it's still a waste of weirdness budget on something which is pretty minor. There are lots of traits which are accepting unsized arguments and there wouldn't have been any need to have unique syntax just for one weird corner-case if all generic types were always, unconditionally, ?Sized (except if explicitly specified as : Sized).

Alternatively one may imagine yet another special rule where Trait would have Self auto-Sized if at least one methods accepts Self (and not &Self or &mut Self).

Yet this is not a requirement (as current thread shows) and, in practice, the fact that many crates want &dyn Something + 'static is major PITA. Sometimes they require that 'static simply because it's a default and authors of the crate weren't even thinking about whether they can accept something non-'static or not. Like topicstarter did and only found out the issue when he asked on URLO.

Not every Rustacean would go visit URLO in such a case which mean many of them wouldn't even know that &dyn Something doesn't require 'static, that's it's just a default.

If they are not legitemately surprising then why people come to URLO to discuss them? They weren't coming here to discuss fine details of language specifications, they wanted to solve the problem. It wouldn't have existed or was solved without such visits if behavior would have been nonsurprising.

Please don't mix surprising with “made consciously because of reasons”. These are different things. You don't need to read hundred-messages long discussions to understand something that's not legitemately surprising. Even if the decisions picked was the best possible at the time it still doesn't mean it not surprising.

Yes, but if even people with years of experience couldn't reliably predict what would be the end result… its surprising. It maybe logical, mathematically justified, etc — yet it's still surprising if people couldn't reliable give answer to the appropriate quiz.

On that we can agree. Let's assume I was talking about “weird” if you don't like the world “inconsistent”.

To me the fact that this:

    let mut baz = bar;
    baz.0.0 = 42;

works radically differently from that:

    let Test(mut baz) = bar;
    baz.0 = 42;

Doesn't look consistent. And yes, I know why these similar things work radically differently. Because they only look similar but in fact are entirely different things. Similarly to closure vs match arm confusion.

But closure vs match arm is, mostly, a theoretical issue this inconsistency. I have never seen it in real code. Yet with mut, if you decide to add destructuring and would try the following (pretty obvious for newbie attempt to unwrap the wrapper):

    let mut Test(baz) = bar;
    baz.0 = 42;

Compiler would say it's incorrect and propose a helpful change:

error : `mut` must be attached to each individual binding
--> <source>:4:9
  |
4 | let mut Test(baz) = bar;
  | ^^^^^^^^^^^^^ help: add `mut` to each binding: `Test(mut baz)`
  |
= note: `mut` may be followed by `variable` and `variable @ pattern`
error : aborting due to previous error

If you follow compiler suggestion, then, indeed, error goes away… but code doesn't work.

Instead, to be able to actually modify foo via baz you have to use:

    let Test(baz) = bar;
    baz.0 = 42;

How is that consistent? And if it's consistent then why compiler can not suggest the sensible thing (maybe you want to remove mut from this code because it's not really needed since mutability for baz is implied here) is not even mentioned?

P.S. You may say that confusion started when useless mut was used in the original version, but then why compiler is happy with it and doesn't propose to remove it?

Because they don't know the language yet. The majority of people asking questions here are beginners, and as such, the overwhelming majority of questions are about simple things. Any feature, however benign, well-designed or consistent, might be confusing to a beginner, so "there are a lot of questions asked about X" doesn't mean that "X is hard".

People ask about all sorts of trivial things on URLO, ranging from number <-> string conversion through 1st-year single-loop algorithms to understanding some terminology in the documentation. Yet this absolutely isn't an argument supporting "ToString is hard" or "for loops are hard".

I'm not a newbie to Rust: I've written probably about 2-300kloc over the last few years, across various work projects and personal experiments, including way too much unsafe, FFI and async trait nonsense.

I have very little confidence I could have diagnosed the issue here particularly quickly: I vaguely know traits can default to + 'static, but I haven't run into it enough (or for it to be enough of a problem) to immediately know what the problem was, or have a good grasp of when and why.

There's not really any such thing as "newbie" problems, just ones you've hit or haven't hit.

3 Likes

Actually there are: if problem comes from violation of basic rules which you learn in any tutorial then you can call it “beginner's problem”.

If problem comes from violation of some obscure rules which are not even mentioned in most tutorials (can you show me where the rule “dyn implies 'static” is mentioned in the book?) it's not a newbie problem, sorry.

I only remember one place there dyn Fn() + Send + 'static is mentioned which strongly hint's, to me, at least, that 'static bound have to be added explicitly… the exact opposite from what happens in reality).

Also note that we, again, faced very similar issue with GATs. Remember? That add the required where clause: where Self: 'a.

Even the wording of the error message strongly implies that it's something that you have to do, not something that may want to do.

Yet there are no special rule which makes that declaration unnecessary. Instead compiler's error message was tweaked a bit. Why this time haven't we got yet-another-obsure-rule, hmm?

I want to be a bit clearer here: just I mean that there's not fundamentally any difference between when there's something you don't know when you have used a language for two minutes and when you've used a language for two years: it's more about how likely it is that you've seen any specific thing before than how difficult it is to understand.

There are big difference between something you may explain by just using rules that you already know and arbitrary adjustment to the rules which are never mentioned anywhere.

Try to find that information about the fact that 'dyn Trait without explicit bound implies static. I couldn't find it a reference guide, I couldn't find it in tutorial, the only place where it's mentioned is in some forum threads.

And it's arbitrary! You couldn't say “we needed to add that rule because there are no other alternative”. There were.

Maybe they weren't as nice or maybe they weren't as obvious, but that rule definitely goes against Rust attempt to ensure that it wouldn't turn out into shell (where every “obvious” attempt to do something end up wrong and you need to do crazy things to hope to handle things correctly).

I would much prefer to specify that 'static bond explicitly where I want it if alternative is that people are getting it implicitly and don't even realize it's there.

This feels sufficiently non-obvious to me, after 5 years of heavy Rust use, that I've filed Casting to `dyn Trait` implies `'static` lifetime, but "lifetime may not live long enough" does not prompt · Issue #104197 · rust-lang/rust · GitHub to ask for a better diagnostic.

I think that an experienced programmer might not spot this if they've not used dyn Trait recently, and a hint that dyn Trait is short for dyn Trait + 'static would help identify why the compiler's talking about 'static in the error message.

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.