¿Por qué no es `std :: mem :: drop` exactamente igual que el cierre | _ | () en los límites de rasgos de mayor rango?

13

La implementación de std::mem::dropestá documentada para ser la siguiente:

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

Como tal, esperaría que el cierre |_| ()(conocido coloquialmente como cierre del inodoro ) sea un posible reemplazo 1: 1 drop, en ambas direcciones. Sin embargo, el siguiente código muestra que dropno es compatible con un rasgo de mayor rango vinculado al parámetro de la función, mientras que el cierre del inodoro sí.

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

El mensaje de error del compilador:

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

Teniendo en cuenta que dropes supuestamente genérico con respecto a cualquier tamaño T, no parece razonable que la firma "más genérica" fn(_) -> _no sea compatible for<'a> fn (&'a _) -> _. ¿Por qué el compilador no admite la firma de dropaquí, y qué lo hace diferente cuando el cierre del inodoro se coloca en su lugar?

E_net4 el downvoter
fuente

Respuestas:

4

El núcleo del problema es que dropno se trata de una sola función, sino de un conjunto de funciones parametrizadas, cada una de las cuales descarta algún tipo en particular. Para satisfacer un límite de rasgo de mayor rango (en lo sucesivo hrtb), necesitaría una única función que pueda tomar referencias a un tipo de forma simultánea con una vida útil determinada.


Lo usaremos dropcomo nuestro ejemplo típico de una función genérica, pero todo esto también se aplica de manera más general. Aquí está el código de referencia: fn drop<T>(_: T) {}.

Conceptualmente, dropno es una función única, sino una función para cada tipo posible T. Cualquier instancia particular de droptoma solo argumentos de un solo tipo. Esto se llama monomorfización . Si Tse usa dropuna diferente, dropse compila una versión diferente de . Es por eso que no puede pasar una función genérica como argumento y usar esa función en general (vea esta pregunta )

Por otro lado, una función como fn pass(x: &i32) -> &i32 {x}satisface la hrtb for<'a> Fn(&'a i32) -> &'a i32. A diferencia drop, tenemos una única función que satisface simultáneamente Fn(&'a i32) -> &'a i32para toda la vida 'a. Esto se refleja en cómo passse puede usar.

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);
}

(patio de recreo)

En el ejemplo, las vidas 'ay 'bno tienen relación entre sí: ninguna abarca completamente a la otra. Entonces no hay algún tipo de subtipo aquí. Una sola instancia depass realmente se está utilizando con dos vidas diferentes y no relacionadas.

Por eso dropno satisface for<'a> FnOnce(&'a T). Cualquier instancia particular de dropsolo puede cubrir una vida (ignorando el subtipo). Si pasamos dropen two_usesel ejemplo anterior (con cambios de compás ligeras y asumiendo el compilador nos dejó), que tendría que elegir alguna vida en particular 'ay la instancia de dropen el ámbito de two_usesque sería Fn(&'a i32)por alguna concreta de por vida 'a. Dado que la función solo se aplicaría a una sola vida útil 'a, no sería posible usarla con dos vidas no relacionadas.

Entonces, ¿por qué el cierre del inodoro recibe un hrtb? Al inferir el tipo para un cierre, si el tipo esperado sugiere que se necesita un límite de rasgo de mayor rango, el compilador intentará ajustarlo . En este caso, tiene éxito.


El problema # 41078 está estrechamente relacionado con esto y, en particular, el comentario de eddyb aquí da esencialmente la explicación anterior (aunque en el contexto de los cierres, en lugar de las funciones ordinarias). Sin embargo, el problema en sí no aborda el problema actual. En cambio, aborda lo que sucede si asigna el cierre del inodoro a una variable antes de usarlo (¡pruébelo!).

Es posible que la situación cambie en el futuro, pero requeriría un cambio bastante grande en cómo se monomorfizan las funciones genéricas.

SCappella
fuente
4

En resumen, ambas líneas deberían fallar. Pero dado que un paso en la antigua forma de manejar las vidas de hrtb, es decir, la verificación de fugas , actualmente tiene algún problema de solidez, rustctermina aceptando (incorrectamente) uno y dejando al otro con un mensaje de error bastante malo.

Si desactiva la comprobación de fugas con rustc +nightly -Zno-leak-check, podrá ver un mensaje de error más sensible:

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

Mi interpretación de este error es que &xen el cuerpo de la foofunción solo tiene una vida útil limitada a dicho cuerpo, por lo que f(&x)también tiene la misma vida útil que no puede satisfacer la for<'a>cuantificación universal requerida por el rasgo limitado.

La pregunta que presenta aquí es casi idéntica a la cuestión # 57642 , que también tiene dos partes contrastantes.

La nueva forma de procesar vidas de hrtb es mediante el uso de los llamados universos . Niko tiene un WIP para abordar la verificación de fugas con universos. Bajo este nuevo régimen, se dice que ambas partes del problema # 57642 vinculado anteriormente fallan con diagnósticos mucho más claros. Supongo que el compilador también debería poder manejar su código de ejemplo correctamente para entonces.

edwardw
fuente