Michael Snoyman
Michael Snoyman

Reputation: 31355

Is there a way to pass a reference to a generic function and return an impl Trait that isn't related to the argument's lifetime?

I've worked down a real-life example in a web app, which I've solved using unnecessary heap allocation, to the following example:

// Try replacing with (_: &String)
fn make_debug<T>(_: T) -> impl std::fmt::Debug {
    42u8
}

fn test() -> impl std::fmt::Debug {
    let value = "value".to_string();

    // try removing the ampersand to get this to compile
    make_debug(&value)
}

pub fn main() {
    println!("{:?}", test());
}

As is, compiling this code gives me:

error[E0597]: `value` does not live long enough
  --> src/main.rs:9:16
   |
5  | fn test() -> impl std::fmt::Debug {
   |              -------------------- opaque type requires that `value` is borrowed for `'static`
...
9  |     make_debug(&value)
   |                ^^^^^^ borrowed value does not live long enough
10 | }
   | - `value` dropped here while still borrowed

I can fix this error in at least two ways:

  1. Instead of passing in a reference to value in test(), pass in value itself
  2. Instead of the parameter T, explicitly state the type of the argument for make_debug as &String or &str

My understanding of what's happening is that, when there is a parameter, the borrow checker is assuming that any lifetime on that parameter affects the output impl Debug value.

Is there a way to keep the code parameterized, continue passing in a reference, and get the borrow checker to accept it?

Upvotes: 5

Views: 1743

Answers (1)

bitemyapp
bitemyapp

Reputation: 1647

I think this is due to the rules around how impl trait opaque types capture lifetimes.

If there are lifetimes inside an argument T, then an impl trait has to incorporate them. Additional lifetimes in the type signature follow the normal rules.

For more information please see:

A more complete answer

Original goal: the send_form function takes an input parameter of type &T which is rendered to a binary representation. That binary representation is owned by the resulting impl Future, and no remnant of the original &T remains. Therefore, the lifetime of &T need not outlive the impl Trait. All good.

The problem arises when T itself, additionally, contains references with lifetimes. If we were not using impl Trait, our signature would look something like this:

fn send_form<T>(self, data: &T) -> SendFormFuture;

And by looking at SendFormFuture, we can readily observe that there is no remnant of T in there at all. Therefore, even if T has lifetimes of its own to deal with, we know that all references are used within the body of send_form, and never used again afterward by SendFormFuture.

However, with impl Future as the output, we get no such guarantees. There's no way to know if the concrete implementation of Future in fact holds onto the T.

In the case where T has no references, this still isn't a problem. Either the impl Future references the T, and fully takes ownership of it, or it doesn't reference it, and no lifetime issues arise.

However, if T does have references, you could end up in a situation where the concrete impl Future is holding onto a reference stored in the T. Even though the impl Future has ownership of the T itself, it doesn't have ownership of the values referenced by the T.

This is why the borrow check must be conservative, and insist that any references inside T must have a 'static lifetime.

The only workaround I can see is to bypass impl Future and be explicit in the return type. Then, you can demonstrate to the borrow checker quite easily that the output type does not reference the input T type at all, and any references in it are irrelevant.

The original code in the actix web client for send_form looks like:

https://docs.rs/awc/0.2.1/src/awc/request.rs.html#503-522

pub fn send_form<T: Serialize>(
        self,
        value: &T,
    ) -> impl Future<
        Item = ClientResponse<impl Stream<Item = Bytes, Error = PayloadError>>,
        Error = SendRequestError,
    > {
        let body = match serde_urlencoded::to_string(value) {
            Ok(body) => body,
            Err(e) => return Either::A(err(Error::from(e).into())),
        };

        // set content-type
        let slf = self.set_header_if_none(
            header::CONTENT_TYPE,
            "application/x-www-form-urlencoded",
        );

        Either::B(slf.send_body(Body::Bytes(Bytes::from(body))))
    }

You may need to patch the library or write your own function that does the same thing but with a concrete type. If anyone else knows how to deal with this apparent limitation of impl trait I'd love to hear it.

Here's how far I've gotten on a rewrite of send_form in awc (the actix-web client library):

    pub fn send_form_alt<T: Serialize>(
        self,
        value: &T,
        // ) -> impl Future<
        //     Item = ClientResponse<impl Stream<Item = Bytes, Error = PayloadError>>,
        //     Error = SendRequestError,
    ) -> Either<
        FutureResult<String, actix_http::error::Error>,
        impl Future<
            Item = crate::response::ClientResponse<impl futures::stream::Stream>,
            Error = SendRequestError,
        >,
    > {

Some caveats so far:

  • Either::B is necessarily an opaque impl trait of Future.
  • The first param of FutureResult might actually be Void or whatever the Void equivalent in Rust is called.

Upvotes: 2

Related Questions