Is there any way to set a trait object lifetime “as long as it can be”?

I am familiar with the rules of trait object lifetime bounds, and how they may be elided. This is not a question about how the language works, but rather if there’s a clever trick to get what I want. Consider the following two modules trying to do similar things:

pub mod a {
    use std::sync::{Arc, Mutex};

    pub struct Holder<T> {
        // The Mutex exists for reasons that are out of scope.
        // It is included just to clarify that `Holder<T>` is already invariant over `T`.
        data: Arc<Mutex<T>>,
    }
    
    impl<T: Clone> Holder<T> {
        pub fn constant(value: T) -> Self {
            Holder { data: Arc::new(Mutex::new(value)) }
        }
        
        pub fn get(&self) -> T {
            self.data.lock().unwrap().clone()
        }
    }
}

pub mod b {
    use std::sync::Arc;

    pub trait DataSource<T> {
        fn get(&self) -> T;
    }

    pub struct Holder<T> {
        data: Arc<dyn DataSource<T>>,
    }
    
    impl<T: Clone> Holder<T> {
        pub fn constant(value: T) -> Self {
            struct Constant<T>(T);
            impl<T: Clone> DataSource<T> for Constant<T> {
                fn get(&self) -> T {
                    self.0.clone()
                }
            }
            Holder { data: Arc::new(Constant(value)) }
        }
        
        pub fn get(&self) -> T {
            self.data.get()
        }
    }
}

These are intended to serve the same purposes (handles to data which may change over time, which allow reading that data at any point), except that b uses a trait so that it can be extended to more kinds of data sources, whereas a is fixed to one representation. However, b does not compile:

error[E0310]: the parameter type `T` may not live long enough
  --> src/lib.rs:39:28
   |
39 |             Holder { data: Arc::new(Constant(value)) }
   |                            ^^^^^^^^^^^^^^^^^^^^^^^^^
   |                            |
   |                            the parameter type `T` must be valid for the static lifetime...
   |                            ...so that the type `T` will meet its required lifetime bounds
   |
help: consider adding an explicit lifetime bound
   |
31 |     impl<T: Clone + 'static> Holder<T> {
   |                   +++++++++

This error makes sense, but is there any way to solve it without changing the signature of Holder? In particular, without adding a 'static bound and without adding a lifetime parameter Holder<'a, T>? What I would like is for the dyn DataSource to somehow automatically get a lifetime filled in which is exactly T’s validity (the intersection of T’s contained lifetimes?), so that “T always lives long enough”. Is there a way to persuade the type system to make that happen? I would really like to add the trait shown in b while having an API surface otherwise identical to a.

I wouldn’t know any way this is possible, unfortunately.

HRTBs seem to work.

This restricts T to T: 'static

Maybe with a little bit of unsafe you can do the following:

use std::sync::Arc;

pub trait DataSource<T> {
    fn get(&self) -> T;

    // typically, implement this by returning `this`
    // `DynDataSource` uses this to know that the returned
    // trait object will live for all lifetimes `'a` for which `T` lives 
    fn arc_to_dyn<'a>(this: Arc<Self>) -> Arc<dyn DataSource<T> + 'a>
    where
        Self: Sized,
        T: 'a;
}

mod encapsulate_lifetime {
    use super::*;
    // the `'static` is a lie :-)
    #[repr(transparent)]
    pub struct DynDataSource<T>(dyn DataSource<T> + 'static);
    impl<T> DynDataSource<T> {
        pub fn new_arc(source: Arc<impl DataSource<T>>) -> Arc<Self> {
            let arc = DataSource::arc_to_dyn(source);
            unsafe { std::mem::transmute::<Arc<dyn DataSource<T>>, Arc<Self>>(arc) }
        }
    }
    impl<T> DataSource<T> for DynDataSource<T> {
        fn get(&self) -> T {
            self.0.get()
        }

        fn arc_to_dyn<'a>(_: Arc<Self>) -> Arc<dyn DataSource<T> + 'a>
        where
            Self: Sized,
            T: 'a,
        {
            unreachable!("Self: Sized can't be fulfilled")
        }
    }
}
use encapsulate_lifetime::DynDataSource;

pub struct Holder<T> {
    data: Arc<DynDataSource<T>>,
}

impl<T: Clone> Holder<T> {
    pub fn constant(value: T) -> Self {
        struct Constant<T>(T);
        impl<T: Clone> DataSource<T> for Constant<T> {
            fn get(&self) -> T {
                self.0.clone()
            }

            fn arc_to_dyn<'a>(this: Arc<Self>) -> Arc<dyn DataSource<T> + 'a>
            where
                Self: Sized,
                T: 'a,
            {
                this
            }
        }
        Holder {
            data: DynDataSource::new_arc(Arc::new(Constant(value))),
        }
    }

    pub fn get(&self) -> T {
        self.data.get()
    }
}

I wouldn’t want the original dyn DataSource<T> trait object publicly visible, because even though it’s pretty safe that T will always be invariant [feel free to add a PhantomData to DynDataSource<T> anyways if you want that doubly enforced],
it’s definitely a future possibility that upcasts from dyn DataSource<T> + 'a to something like dyn Destruct + 'a could become allowed ~ or that the language gets extended so that not all type parameters that appear syntactically actually in a type Type need to be considered for Type: 'a bounds, which would allow for dyn DataSource<T> + 'static: 'static.

But encapsulated like in the code above (with only the delegating trait implementation of DataSource), it should be pretty sound in a future-proof manner.

2 Likes

I have wanted that too, and believe it would have been a more sensible dyn lifetime default than 'static. But there is no syntax for it.

If you use unsafe, the tricky part is when the erased type has less validity than T. Perhaps particularly if T: 'static.

Thanks for the input so far. I'm currently thinking that the right route is to get rid of (the thing in my real problem that corresponds to) Holder and let users deal with the trait objects (or generics) directly. Unfortunate, but more flexible than requiring 'static and less fragile than @steffahn’s unsafe lifetime erasure.