The implementation of std::mem::drop
is documented to be the following:
pub fn drop<T>(_x: T) { }
As such, I would expect the closure |_| ()
(colloquially known as the toilet closure) to be a potential 1:1 replacement to drop
, in both directions. However, the code below shows that drop
isn't compatible with a higher ranked trait bound on the function's parameter, whereas the toilet closure is.
fn foo<F, T>(f: F, x: T)
where
for<'a> F: FnOnce(&'a T),
{
dbg!(f(&x));
}
fn main() {
foo(|_| (), "toilet closure"); // this compiles
foo(drop, "drop"); // this does not!
}
The compiler's error message:
error[E0631]: type mismatch in function arguments
--> src/main.rs:10:5
|
1 | fn foo<F, T>(f: F, x: T)
| ---
2 | where
3 | for<'a> F: FnOnce(&'a T),
| ------------- required by this bound in `foo`
...
10 | foo(drop, "drop"); // this does not!
| ^^^
| |
| expected signature of `for<'a> fn(&'a _) -> _`
| found signature of `fn(_) -> _`
error[E0271]: type mismatch resolving `for<'a> <fn(_) {std::mem::drop::<_>} as std::ops::FnOnce<(&'a _,)>>::Output == ()`
--> src/main.rs:10:5
|
1 | fn foo<F, T>(f: F, x: T)
| ---
2 | where
3 | for<'a> F: FnOnce(&'a T),
| ------------- required by this bound in `foo`
...
10 | foo(drop, "drop"); // this does not!
| ^^^ expected bound lifetime parameter 'a, found concrete lifetime
Considering that drop
is supposedly generic with respect to any sized T
, it sounds unreasonable that the "more generic" signature fn(_) -> _
is not compatible with for<'a> fn (&'a _) -> _
. Why is the compiler not admitting the signature of drop
here, and what makes it different when the toilet closure is placed in its stead?
The core of the issue is that drop
is not a single function, but rather a parameterized set of functions that each drop some particular type. To satisfy a higher-ranked trait bound (hereafter hrtb), you'd need a single function that can simultaneously take references to a type with any given lifetime.
We'll use drop
as our typical example of a generic function, but all this applies more generally too. Here's the code for reference: fn drop<T>(_: T) {}
.
Conceptually, drop
is not a single function, but rather one function for every possible type T
. Any particular instance of drop
takes only arguments of a single type. This is called monomorphization. If a different T
is used with drop
, a different version of drop
is compiled. That's why you can't pass a generic function as an argument and use that function in full generality (see this question)
On the other hand, a function like fn pass(x: &i32) -> &i32 {x}
satisfies the hrtb for<'a> Fn(&'a i32) -> &'a i32
. Unlike drop
, we have a single function that simultaneously satisfies Fn(&'a i32) -> &'a i32
for every lifetime 'a
. This is reflected in how pass
can be used.
fn pass(x: &i32) -> &i32 {
x
}
fn two_uses<F>(f: F)
where
for<'a> F: Fn(&'a i32) -> &'a i32, // By the way, this can simply be written
// F: Fn(&i32) -> &i32 due to lifetime elision rules.
// That applies to your original example too.
{
{
// x has some lifetime 'a
let x = &22;
println!("{}", f(x));
// 'a ends around here
}
{
// y has some lifetime 'b
let y = &23;
println!("{}", f(y));
// 'b ends around here
}
// 'a and 'b are unrelated since they have no overlap
}
fn main() {
two_uses(pass);
}
(playground)
In the example, the lifetimes 'a
and 'b
have no relation to each other: neither completely encompasses the other. So there isn't some kind of subtyping thing going on here. A single instance of pass
is really being used with two different, unrelated lifetimes.
This is why drop
doesn't satisfy for<'a> FnOnce(&'a T)
. Any particular instance of drop
can only cover one lifetime (ignoring subtyping). If we passed drop
into two_uses
from the example above (with slight signature changes and assuming the compiler let us), it would have to choose some particular lifetime 'a
and the instance of drop
in the scope of two_uses
would be Fn(&'a i32)
for some concrete lifetime 'a
. Since the function would only apply to single lifetime 'a
, it wouldn't be possible to use it with two unrelated lifetimes.
So why does the toilet closure get a hrtb? When inferring the type for a closure, if the expected type hints that a higher-ranked trait bound is needed, the compiler will try to make one fit. In this case, it succeeds.
Issue #41078 is closely related to this and in particular, eddyb's comment here gives essentially the explanation above (though in the context of closures, rather than ordinary functions). The issue itself doesn't address the present problem though. It instead addresses what happens if you assign the toilet closure to a variable before using it (try it out!).
It's possible that the situation will change in the future, but it would require a pretty big change in how generic functions are monomorphized.
In short, both lines should fail. But since one step in old way of handling hrtb lifetimes, namely the leak check, currently has some soundness issue, rustc
ends up (incorrectly) accepting one and leaving the other with a pretty bad error message.
If you disable the leak check with rustc +nightly -Zno-leak-check
, you'll be able to see a more sensible error message:
error[E0308]: mismatched types
--> src/main.rs:10:5
|
10 | foo(drop, "drop");
| ^^^ one type is more general than the other
|
= note: expected type `std::ops::FnOnce<(&'a &str,)>`
found type `std::ops::FnOnce<(&&str,)>`
My interpretation of this error is that the &x
in the body of the foo
function only has a scope lifetime confined to the said body, so f(&x)
also has the same scope lifetime which can't possibly satisfy the for<'a>
universal quantification required by the trait bound.
The question you present here is almost identical to issue #57642, which also has two contrasting parts.
The new way to process hrtb lifetimes is by using so-called universes. Niko has a WIP to tackle the leak check with universes. Under this new regime, both parts of issue #57642 linked above is said to all fail with far more clear diagnoses. I suppose the compiler should be able to handle your example code correctly by then, too.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With