impl<T> MyStruct<dyn MyTrait<T>> when T has a lifetime parameter

^ Minimal example

What I'm really trying to do is hack around the lack of object-safety for traits with GATs for my one specific trait. If I specialize the impl for each Request<'a> associated type I use it works. And this workaround actually is ok for instantiating the HandlerBox, but I run into the same "impl is not general enough" issue later anyway when blanket implementing the Handler trait for HandlerBox for any given request type.

Been stuck for hours :frowning: Any ideas?

One workaround is offload the GAT to another trait like this.

I'm not sure if this fits your need though.

Thanks for taking a look. I don't think that will work for me since I need to use the lifetime in the Handler trait's method (not shown in the example). Copying my more descriptive write-up from discord:

I have a trait that has an associated type with a generic lifetime (well, a couple) that looks like this:

pub trait AsyncHandlerFifo {
    type Request<'a>: Decode<'a>;
    type Response: Encode;
    type Future<'a>: Future<Output = Self::Response> + 'a where Self: 'a;

    fn call<'a>(
        &'a mut self,
        requester: EndpointAddr,
        payload: Self::Request<'a>,
    ) -> Self::Future<'a>;

And I'm trying to manually create a box type for it so I can have a trait-object despite the no-object-safety-for-GATs-yet restriction. I manually create a vtable with call and drop methods and allocate a re-usable slot on the heap for the returned future (and return as &dyn Future to erase underlying future type without resorting to a fresh Box on every call) - since call mutably borrows self and the returned future captures the borrow, I figure re-using the same allocation for each call should be safe. Anyway I've got it all implemented and it seems to work in a local test but if I try to use it with some generic framework-ey code like:

fn register_handler<H: AsyncRequestHandler>(h: H) { ... }

I get "implementation of AsyncHandlerFifo is not general enough". I guess I'm implicitly hoping the following blanket impl for my custom box type will implement for any lifetime if Req type param takes a lifetime parameter, and that appears to not be the case.

impl<Req, Resp> AsyncHandlerFifo for AsyncHandlerFifoBox<dyn AsyncHandlerFifoObject<Req, Resp>>
        Req: for<'a> Decode<'a>,
        Resp: Encode,
{ ... }

Here I'm using a dummy AsyncHandlerFifoObject trait to attach the request and response types to the custom box without attaching lifetimes to the box. e.g. AsyncHandlerFifoBox<dyn for<'a> AsyncHandlerFifoObject<Request<'a> = &'a str, Response = String>>

Binders don't switch levels; you don't automatically get an implementation for the higher-ranked type (dyn for<'a> Obj<&'a str>) even if you have an implementation that covers the same set of types in a non-higher-ranked fashion (every dyn Obj<T>). You can see this with something like:

trait Ex {}
impl<R> Ex for dyn HandlerObject<R> {}
impl Ex for dyn for<'a> HandlerObject<&'a str> {}

The implementations are allowed to coexist, even though one of the types is a subtype of the other. [1] To paraphrase Ariel,

dyn for<'a> HandlerObject<&'a str> is not equal to dyn HandlerObject<R> for any R (after all, what would that R be? for<'a> &'a str? That is not a type, and would give us dyn HandlerObject<for<'a> &'a str>, which has the for in the wrong place).

(Now, the example does come with a scary warning pointing to issue 56105 and saying it will become a hard error. But to the best of my knowledge, that's a bald-faced lie; the current direction is to continue accepting them. [2])

Your situation is a little different in that you're trying to induce some sort of equality as a type parameter, versus as an implementer, but I believe it comes down to the same distinction.

  1. The more typical example is fn(T) vs fn(&T) -- aka for<'any> fn(&'any T). ↩ī¸Ž

  2. Here's another thread with probably way more discussion about it than you care to read; I'm mostly putting a link in here in case I need the breadcrumbs. ↩ī¸Ž

1 Like

Would this not work for you? Ala here.

Edit: With an addition to make the Box<...> implement the original trait.

Oh wow the shadow trait trick is pretty clever. It looks like in the end we run into the same issue which I guess is that while you can write an impl for any given type

impl<T> HandlerBox<dyn HandlerObject<T>> { }

You cannot write an impl for any given type constructor that takes a lifetime argument:

impl<T<'a>> HandlerBox<dyn for<'a> HandlerObject<T<'a>>> { }

Except if T<'a> happens to be &'a T, then you can do what you did to make "Box<...> implement the original trait". You can't do that for a custom struct HmmRequest<'a> for example AFAICT (well, for any given struct that takes a single lifetime parameter - you can write an impl for each of the structs individually).

For now what I've done is just written a macro impl AsyncHandlerFifo for AsyncHandlerFifoBox<dyn for<'a> AsyncHandlerFifoObject<$req_ty<'a>, $resp_ty>>, it's not too painful to manually invoke the macro for each request handler type I want to box.

Thanks for looking at this with me!

1 Like

Right, that's a great way to put it.

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.