valor - ¿Cómo sabe Rust si ejecutar el destructor durante el desenrollado de la pila?
macros en excel (3)
Hay dos preguntas escondidas aquí:
- ¿Cómo el compilador realiza un seguimiento de qué variable se inicializa o no?
- ¿Por qué la inicialización con
mem::uninitialized()
llevar a un comportamiento indefinido?
Vamos a abordarlos en orden.
¿Cómo el compilador realiza un seguimiento de qué variable se inicializa o no?
El compilador inyecta los llamados "indicadores de caída": para cada variable para la que se debe ejecutar Drop
al final del alcance, se inyecta un indicador booleano en la pila, que indica si esta variable debe eliminarse.
La bandera comienza con "no", se mueve a "sí" si la variable está inicializada, y vuelve a "no" si la variable se mueve desde.
Finalmente, cuando llega el momento de eliminar esta variable, se marca la bandera y se elimina si es necesario.
Esto no tiene relación con si el análisis de flujo del compilador se queja de variables potencialmente no inicializadas: solo cuando se satisface el análisis de flujo se genera un código.
¿Por qué la inicialización con
mem::uninitialized()
llevar a un comportamiento indefinido?
Cuando usas mem::uninitialized()
haces una promesa al compilador: no te preocupes, definitivamente estoy inicializando esto .
En lo que concierne al compilador, la variable está por lo tanto completamente inicializada, y el indicador de caída se establece en "sí" (hasta que salga de él).
Esto, a su vez, significa que se llamará Drop
.
El uso de un objeto sin inicializar es un comportamiento indefinido, y el compilador que llama a Drop
en un objeto sin inicializar en su nombre cuenta como "usándolo".
Prima:
En mis pruebas, no pasó nada raro!
Tenga en cuenta que el comportamiento indefinido significa que cualquier cosa puede suceder; todo lo que, desafortunadamente, también incluye "parece funcionar" (o incluso "funciona según lo previsto a pesar de las probabilidades").
En particular, si NO accede a la memoria del objeto en Drop::drop
(solo imprimiendo), entonces es muy probable que todo funcione. Sin embargo, si accedes a él, es posible que veas enteros extraños, punteros que apuntan a lo salvaje, etc.
Y si el optimizador es inteligente, incluso sin acceder a él, ¡podría hacer cosas raras! Ya que estamos usando LLVM, lo invito a leer Lo que todo programador de C debería saber sobre el comportamiento indefinido por Chris Lattner (el padre de LLVM).
La documentación de mem::uninitialized
señala por qué es peligroso / inseguro usar esa función: llamar a drop
en la memoria no inicializada es un comportamiento indefinido.
Así que este código debería ser, creo, indefinido:
let a: TypeWithDrop = unsafe { mem::uninitialized() };
panic!("=== Testing ==="); // Destructor of `a` will be run (U.B)
Sin embargo, escribí este fragmento de código que funciona en Rust seguro y no parece sufrir un comportamiento indefinido:
#![feature(conservative_impl_trait)]
trait T {
fn disp(&mut self);
}
struct A;
impl T for A {
fn disp(&mut self) { println!("=== A ==="); }
}
impl Drop for A {
fn drop(&mut self) { println!("Dropping A"); }
}
struct B;
impl T for B {
fn disp(&mut self) { println!("=== B ==="); }
}
impl Drop for B {
fn drop(&mut self) { println!("Dropping B"); }
}
fn foo() -> impl T { return A; }
fn bar() -> impl T { return B; }
fn main() {
let mut a;
let mut b;
let i = 10;
let t: &mut T = if i % 2 == 0 {
a = foo();
&mut a
} else {
b = bar();
&mut b
};
t.disp();
panic!("=== Test ===");
}
Siempre parece ejecutar el destructor correcto, mientras ignora el otro. Si intenté usar a
o b
(como a.disp()
lugar de t.disp()
), se t.disp()
error correctamente al decir que posiblemente esté usando memoria no inicializada. Lo que me sorprendió es que mientras panic
rey del panic
, siempre ejecuta el destructor correcto (imprimiendo la cadena esperada) sin importar el valor de i
.
¿Como sucedió esto? Si el tiempo de ejecución puede determinar qué destructor se ejecutará, ¿debería eliminarse la parte sobre la memoria que necesariamente se debe inicializar para los tipos con Drop
implementado de la documentación de mem::uninitialized()
como se vinculó anteriormente?
Primero, hay indicadores de caída : información de tiempo de ejecución para rastrear qué variables se han inicializado. Si una variable no fue asignada a, drop()
no se ejecutará para ella.
En estable, el indicador de caída se almacena actualmente dentro del propio tipo. Escribirle una memoria no inicializada puede causar un comportamiento indefinido en cuanto a si se llamará o no a drop()
. Esta información pronto estará desactualizada porque el indicador de caída se mueve fuera del tipo en sí mismo cada noche.
En el Rust nocturno, si asigna memoria no inicializada a una variable, sería seguro asumir que se ejecutará drop()
. Sin embargo, cualquier implementación útil de drop()
operará en el valor. No hay forma de detectar si el tipo está correctamente inicializado o no dentro de la implementación del rasgo Drop
: podría resultar en intentar liberar un puntero no válido o cualquier otra cosa aleatoria, dependiendo de la implementación del tipo Drop
. La asignación de memoria no inicializada a un tipo con Drop
es aconsejable de todos modos.
Usando banderas de caída
Rust (hasta la versión 1.12 incluida) almacena una bandera booleana en cada valor cuyo tipo implementa Drop
(y, por lo tanto, aumenta el tamaño de ese tipo en un byte). Esa bandera decide si ejecutar el destructor. Así que cuando haces b = bar()
establece la bandera para la variable b
, y por lo tanto solo ejecuta el destructor de b
. Viceversa con a
.
Tenga en cuenta que a partir de la versión 1.13 de Rust (al momento de escribir esto el compilador beta) esa marca no se almacena en el tipo, sino en la pila para cada variable o temporal. Esto es posible gracias a la llegada del MIR en el compilador Rust. El MIR simplifica significativamente la traducción del código de Rust al código de la máquina y, por lo tanto, habilitó esta función para mover banderas de caída a la pila. Las optimizaciones generalmente eliminarán esa bandera si pueden calcular en tiempo de compilación cuándo se eliminará qué objeto.
Puede "observar" esta bandera en un compilador Rust hasta la versión 1.12 mirando el tamaño del tipo:
struct A;
struct B;
impl Drop for B {
fn drop(&mut self) {}
}
fn main() {
println!("{}", std::mem::size_of::<A>());
println!("{}", std::mem::size_of::<B>());
}
imprime 0
y 1
respectivamente antes de las banderas de pila, y 0
y 0
con las banderas de pila.
Sin embargo, el uso de mem::uninitialized
todavía mem::uninitialized
es seguro, porque el compilador sigue viendo la asignación a la variable a y establece el indicador de caída. Así se llamará al destructor en la memoria no inicializada. Tenga en cuenta que en su ejemplo, la aplicación Drop
no tiene acceso a ninguna memoria de su tipo (excepto el indicador de caída, pero eso es invisible para usted). Por lo tanto, no está accediendo a la memoria sin inicializar (que de todos modos tiene un tamaño de cero bytes, ya que su tipo es una estructura de tamaño cero). Por lo que yo sé, eso significa que su código unsafe { std::mem::uninitialized() }
es realmente seguro, porque después no puede ocurrir ninguna falta de seguridad en la memoria.