为什么`std :: mem :: drop`与排名较高的特征范围中的闭包| _ |()不完全相同?

时间:2019-11-25 00:00:17

标签: rust closures traits type-inference higher-kinded-types

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

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

这样,我希望闭包|_| ()(俗称toilet closure)在两个方向上都可能是drop的1:1替代。但是,下面的代码显示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都是通用的,听起来“通用”签名fn(_) -> _for<'a> fn (&'a _) -> _不兼容是不合理的。为什么编译器在这里不接受drop的签名?当代替抽水马桶盖时,它有什么不同?

2 个答案:

答案 0 :(得分:4)

问题的核心是drop不是单个函数,而是一组参数化的函数,每个函数都删除某些特定类型。为了满足更高级别的特征范围(此后为hrtb),您需要一个 single 函数,该函数可以同时引用对任何给定生命期的类型的引用。


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

从概念上讲,drop不是单个函数,而是每种可能类型T的一个函数。 drop的任何特定实例仅采用单一类型的参数。这称为monomorphization。如果T使用了不同的drop,则会编译不同版本的drop。这就是为什么您不能将通用函数作为参数传递而不能完全通用地使用该函数(请参见this question

另一方面,类似fn pass(x: &i32) -> &i32 {x}的函数可以满足hrtb for<'a> Fn(&'a i32) -> &'a i32的要求。与drop不同,我们有一个 single 函数,在每个 生存期Fn(&'a i32) -> &'a i32的同时满足'a。这反映在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的任何特定实例只能覆盖一个生存期(忽略子类型)。如果我们从上面的示例中将drop传递到two_uses中(签名略有更改,并假设编译器允许我们这样做),则必须选择一些特定的生存期'a和{{ drop范围内的1}}在某些具体生存期two_uses内将为Fn(&'a i32)。由于该功能仅适用于单个生存期'a,因此无法在两个不相关的生存期中使用它。

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


Issue #41078与此密切相关,特别是eddyb的评论here基本上给出了上面的解释(尽管在闭包的上下文中,而不是在普通函数中)。问题本身并未解决当前的问题。相反,它解决了如果您在使用抽水马桶盖之前将其分配给变量(尝试一下!)会发生什么情况。

情况可能会在未来发生变化,但将需要对泛型函数的单态化进行很大的改变。

答案 1 :(得分:4)

简而言之,两行都应该失败。但是,由于处理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,)>`

我对此错误的解释是,&x函数主体中的foo仅具有作用域生存期,限制在该主体中,因此f(&x)也具有相同的作用域生存期这可能无法满足特征绑定所需的for<'a>通用量化。

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

处理hrtb生存期的新方法是使用所谓的universes。 Niko有WIP来解决Universe的泄漏检查。在这种新机制下,上面链接的问题#57642的两个部分都被称为all fail,具有更清晰的诊断。我想到那时编译器也应该能够正确处理您的示例代码。