Reputation: 31355
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:
value
in test()
, pass in value
itselfT
, 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
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:
https://github.com/rust-lang/rust/issues/43396#issuecomment-349716967
https://github.com/rust-lang/rfcs/blob/master/text/1951-expand-impl-trait.md#lifetime-parameters
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
.FutureResult
might actually be Void
or whatever the Void equivalent in Rust is called.Upvotes: 2