ManuallyDrop suprime la invocación automática del compilador de Drop::drop cuando un valor sale del alcance. Al implementar IntoIterator para arreglos o colecciones de tamaño fijo similares, los elementos se extraen mediante ptr::read, que realiza un movimiento a nivel de bits, dejando la memoria fuente lógicamente no inicializada. Sin ManuallyDrop, si ocurre un pánico durante la destrucción de un elemento generado, el mecanismo de deshacer invocaría el destructor del arreglo, intentando eliminar todos los espacios—incluyendo aquellos de los que ya se ha movido—resultando en comportamiento indefinido a través de dobles eliminaciones. Al envolver el almacenamiento en ManuallyDrop, el implementador asume la responsabilidad de eliminar solo los elementos restantes, normalmente rastreando un índice y eliminando manualmente el sufijo en una implementación personalizada de Drop.
Estás construyendo un FixedVec<T, const N: usize>—un vector asignado en pila con capacidad constante—y debes implementar IntoIterator que consume la colección por valor.
El problema central surge durante la extracción de elementos: debes mover cada T del arreglo interno para devolverlo por valor. Si la implementación de T del usuario provoca un pánico durante la destrucción mientras el iterador está parcialmente consumido, el proceso de deshacer aún debe limpiar los elementos restantes. Sin embargo, algunos elementos ya han sido movidos a nivel de bits mediante ptr::read, dejando sus ubicaciones de memoria originales no inicializadas. Si el arreglo de respaldo no está envuelto en ManuallyDrop, su destructor tratará todos los espacios como instancias vivas de T e invocará drop_in_place en ellos, resultando en dobles eliminaciones para elementos movidos (comportamiento indefinido) y potencial uso después de liberaciones.
Solución 1: Usa Option<T> para todos los espacios. Este enfoque almacena Option<T> en el arreglo, permitiéndote take() valores, dejando None detrás. Pros: Completamente seguro, no se requieren bloques de código unsafe, semántica clara. Contras: Sobrecarga de memoria del discriminante (a menudo 1 byte por elemento ajustado al tamaño de palabra), ineficiencia en caché, y requiere inicializar todos los espacios en Some(value) incluso si nunca se utilizan.
Solución 2: Emplea ManuallyDrop para el arreglo. Envuelve el interno [T; N] en ManuallyDrop<[T; N]>. Al generar, lee el valor e incrementa un contador. En el Drop del iterador, elimina manualmente solo el rango restante usando ptr::drop_in_place. Pros: Cero sobrecarga, diseño de memoria idéntico al T crudo, permite manipulación directa de memoria. Contras: Requiere código unsafe, mantenimiento complejo de invariantes respecto a qué espacios están inicializados, riesgo de fugas si la lógica de eliminación manual es incorrecta.
Solución 3: Usa una máscara de validez a nivel de bits. Mantén un conjunto de bits separado que rastree qué índices están vivos. Pros: Sin código unsafe si se utilizan abstracciones seguras para el conjunto de bits. Contras: Complejidad significativa, sobrecarga de manipulación de bits en cada acceso, y patrones de acceso desfavorables para la caché.
Solución Elegida y Resultado: Se seleccionó la Solución 2 para igualar el comportamiento de std::array::IntoIter. La estructura del iterador envuelve el arreglo en ManuallyDrop y rastrea el índice actual. El método next() utiliza ptr::read para mover elementos hacia afuera. La implementación de Drop verifica el índice y llama a ptr::drop_in_place en el segmento restante. Esto asegura que incluso si ocurre un pánico al eliminar un elemento previamente generado, el proceso de deshacer elimina solo el sufijo no tocado, previniendo tanto fugas como dobles eliminaciones. El resultado es una abstracción de costo cero que mantiene invariantes de seguridad de memoria incluso en presencia de destructores que provocan pánicos.
¿Cómo interactúa ManuallyDrop con el trait Copy y por qué puede llevar a errores sutiles al implementar iteradores para tipos Copy?
ManuallyDrop<T> implementa Copy si y solo si T: Copy. Al iterar sobre un arreglo de tipos Copy envueltos en ManuallyDrop, el uso de ptr::read o asignación simple crea copias a nivel de bits en lugar de movimientos. Los candidatos a menudo asumen que ManuallyDrop previene todas las formas de duplicación, pero para tipos Copy, el compilador puede copiar implícitamente el valor cuando tenías la intención de moverlo, llevando a escenarios donde el valor "movido" todavía se considera vivo en la ubicación fuente. Esto puede enmascarar problemas de dobles eliminaciones durante las pruebas con enteros, pero manifestarse como comportamiento indefinido con tipos no Copy. El enfoque correcto es tratar el contenido de ManuallyDrop como movido independientemente de los límites de Copy, o usar ManuallyDrop::into_inner seguido de un reemplazo explícito.
¿Por qué no es suficiente simplemente llamar a mem::forget en el iterador si ocurre un pánico durante la iteración, en lugar de implementar un Drop personalizado que maneje el consumo parcial?
mem::forget consume el iterador sin eliminarlo, lo que de hecho previene la doble eliminación de elementos ya movidos. Sin embargo, también provoca fugas de todos los elementos restantes que no han sido generados, violando las garantías de gestión de recursos que se esperan de las colecciones Rust. El trait Drop existe precisamente para asegurar la limpieza durante el deshacer; confiar en mem::forget en rutas de error transforma un problema de seguridad en una fuga de recursos. El patrón adecuado utiliza ManuallyDrop para deshabilitar la destrucción automática del almacenamiento, luego elimina manualmente solo los elementos no generados en la implementación de Drop, asegurando que no haya fugas ni dobles eliminaciones.
¿Cuál es la distinción entre usar ptr::read para mover de un espacio ManuallyDrop<T> versus usar ManuallyDrop::into_inner, y cuándo es cada uno apropiado en la implementación de iteradores?
ptr::read realiza una copia a nivel de bits del valor y deja la memoria fuente sin cambios (aún conteniendo un T válido), mientras que ManuallyDrop::into_inner consume el envoltorio ManuallyDrop mismo para extraer el valor. En la implementación de iteradores, ptr::read se usa cuando necesitas dejar la cubierta de ManuallyDrop en su lugar (por ejemplo, en un arreglo de ManuallyDrop<T>) para que los espacios restantes aún puedan ser iterados y potencialmente eliminados más tarde. into_inner es apropiado cuando estás consumiendo todo el valor de ManuallyDrop a la vez y no necesitarás rastrear el estado parcial. Usar into_inner en elementos individuales de un arreglo requeriría reenvolver o aritmética compleja de punteros, mientras que ptr::read permite tratar el arreglo como un búfer crudo de datos potencialmente no inicializados.