为什么 `std::mem::drop` 与更高级别特征边界中的闭包 |_|() 不完全相同?

Why isn't `std::mem::drop` exactly the same as the closure |_|() in higher-ranked trait bounds?

std::mem::drop 的实现记录如下:

pub fn drop<T>(_x: T) { }

因此,我希望闭包 |_| ()(俗称 toilet closure)在两个方向上都可能 1:1 替代 drop。但是,下面的代码显示 drop 与函数参数上更高等级的特征绑定不兼容,而厕所关闭是。

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!
}

编译器的错误信息:

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

考虑到 drop 对于任何大小的 T 都是通用的,"more generic" 签名 fn(_) -> _ 与 [=20= 不兼容听起来不合理].为什么编译器在这里不承认 drop 的签名,当马桶盖代替它时有什么不同?

问题的核心是 drop 不是单个函数,而是一组参数化的函数,每个函数都会删除某种特定类型。为了满足更高级别的特征边界(以下称为 hrtb),您需要一个 单个 函数,该函数可以同时引用具有任何给定生命周期的类型。


我们将使用 drop 作为泛型函数的典型示例,但所有这些也适用于更普遍的情况。参考代码如下:fn drop<T>(_: T) {}.

从概念上讲,drop 不是一个单一的函数,而是针对每种可能类型的一个函数 Tdrop 的任何特定实例都只接受单一类型的参数。这叫做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 )

另一方面,像fn pass(x: &i32) -> &i32 {x}这样的函数满足hrtb for<'a> Fn(&'a i32) -> &'a i32。与 drop 不同,我们有一个 单个 函数,它同时满足 every 生命周期 'aFn(&'a i32) -> &'a i32。这反映在如何使用 pass 上。

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)

在示例中,生命周期 'a'b 彼此没有关系:两者都不完全包含对方。所以这里没有发生某种子类型化的事情。 pass 的一个实例实际上被用于两个不同的、不相关的生命周期。

这就是drop不满足for<'a> FnOnce(&'a T)的原因。 drop 的任何特定实例只能涵盖 one 生命周期(忽略子类型)。如果我们从上面的示例中将 drop 传递给 two_uses(稍微更改签名并假设编译器允许我们这样做),它将必须选择某个特定的生命周期 'a 和 [= 的实例two_uses 范围内的 11=] 对于某些 具体 生命周期 'a 将是 Fn(&'a i32)。由于该函数仅适用于单个生命周期 'a,因此不可能将其用于两个不相关的生命周期。

那么为什么马桶盖会出现 hrtb?在推断闭包的类型时,如果预期类型暗示需要更高级别的特征边界,the compiler will try to make one fit。在这种情况下,它成功了。


Issue #41078 is closely related to this and in particular, eddyb's comment here 基本上给出了上面的解释(尽管是在闭包的上下文中,而不是普通函数)。虽然问题本身并没有解决当前的问题。相反,它解决了如果您在使用前将马桶盖分配给一个变量会发生什么(试试看!)。

未来情况可能会发生变化,但这需要对泛型函数的单态化方式进行相当大的改变。

简而言之,这两行都应该失败。但是由于处理 hrtb 生命周期的旧方法的一个步骤,即 the leak check,目前存在一些稳健性问题,rustc 最终(错误地)接受一个并留下一个非常糟糕的错误消息。

如果您使用 rustc +nightly -Zno-leak-check 禁用泄漏检查,您将能够看到更明智的错误消息:

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,)>`

我对这个错误的解释是 foo 函数体中的 &x 的范围生命周期仅限于所述函数体,所以 f(&x) 也有相同的作用域生命周期可能无法满足特征边界要求的 for<'a> 通用量化。

您在此处提出的问题与 issue #57642 几乎相同,后者也有两个不同的部分。

处理 hrtb 寿命的新方法是使用所谓的 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 进行更清晰的诊断。我想编译器到那时也应该能够正确处理您的示例代码。