El rasgo Copy se originó en el diseño temprano de Rust como un marcador para tipos que pueden duplicarse a través de una simple copia bit a bit sin preocupaciones de gestión de recursos. Drop se introdujo para manejar la limpieza de recursos de forma determinista para tipos que gestionan recursos externos como descriptores de archivos o memoria dinámica. El conflicto entre la duplicación implícita y la propiedad única se hizo evidente cuando los diseñadores se dieron cuenta de que las copias bit a bit compartirían manejadores de recursos que no son compartibles. En consecuencia, el compilador fue diseñado para rechazar cualquier tipo que intente implementar ambos rasgos simultáneamente.
Si un tipo que implementa Drop (por ejemplo, gestionando un descriptor de archivo) fuera también Copy, asignar el valor a una nueva variable crearía dos copias bit a bit idénticas. Cuando ambas copias salgan de alcance, la implementación personalizada de Drop se ejecutará dos veces sobre el mismo recurso subyacente. Esto conduce a una vulnerabilidad de double-free o use-after-free si el recurso es invalidado por la primera eliminación pero es accedido por la segunda, comprometiendo la seguridad de la memoria.
El compilador de Rust incluye un chequeo de coherencia en el sistema de rasgos que prohíbe explícitamente a un tipo implementar tanto Copy como Drop. Esta restricción obliga a los desarrolladores a usar Clone (duplicación explícita) para tipos que requieren destrucción personalizada, permitiendo que la implementación incremente adecuadamente los recuentos de referencia o realice copias profundas. Al garantizar que cada entidad lógica tenga una eliminación única correspondiente, el sistema de tipos mantiene abstracciones de coste cero sin sacrificar las garantías de seguridad.
Considera una estructura DatabaseHandle que envuelve un puntero a un objeto de conexión en una biblioteca C externa. La aplicación requiere pasar los manejadores por valor a múltiples closures para registro, pero cada manejador debe cerrar su conexión única a través de una llamada FFI cuando se elimina. Si el manejador fuera Copy, la duplicación implícita crearía múltiples manejadores reclamando la propiedad del mismo recurso C subyacente, lo que inevitablemente causaría cierres dobles o use-after-free cuando salga el alcance.
Un enfoque fue permitir Copy e implementar Drop con conteo de referencias interno utilizando Arc. Esto añadiría sobrecarga de sincronización para cada manejador, aumentando el tamaño del binario y el costo de tiempo de ejecución en todas las operaciones. También complicaría la frontera FFI donde el puntero raw debe ser extraído atómicamente del Arc, introduciendo posibles bloqueos si la lógica de eliminación llama de nuevo al código de Rust.
Otro enfoque involucró usar Copy pero documentar que los usuarios deben llamar a un método close manualmente antes de que se elimine el valor. Esto coloca la carga de la seguridad de la memoria completamente sobre el programador, violando el principio fundamental de Rust de prevenir errores en tiempo de compilación. Inequívocamente conduce a fugas de recursos cuando los desarrolladores olvidan llamar a close, o a cierres dobles cuando copian el manejador inadvertidamente e intentan cerrar ambas copias.
La solución elegida fue eliminar Copy e implementar Clone manualmente, junto con Drop. Clone realiza una copia profunda abriendo una nueva conexión de base de datos, asegurando que cada instancia posea su recurso distinto y previniendo el aliasing del puntero C subyacente. Drop cierra solo su propia conexión, mientras que el compilador previene copias bit a bit accidentales, manteniendo la seguridad sin sobrecarga en tiempo de ejecución.
El sistema de tipos ahora previene la copia accidental en tiempo de compilación, obligando a los desarrolladores a llamar explícitamente a clone y haciendo que la adquisición de recursos sea visible en el código fuente. El programa evita errores de double-free cuando se pasan manejadores a hilos o closures, y las garantías de destrucción determinista permanecen intactas sin requerir operaciones atómicas o gestión manual de memoria.
¿Por qué no puedo derivar Copy para una estructura que contiene un Vec?
Un Vec posee memoria asignada en el heap e implementa Drop para liberar esa memoria cuando el vector sale de alcance. Si una estructura que contiene un Vec fuera Copy, la duplicación bit a bit crearía dos estructuras apuntando al mismo buffer en el heap en la pila, pero ambas contendrían el mismo puntero al heap. Cuando la primera estructura se elimina, la memoria se libera; cuando la segunda se elimina, intenta liberar la misma memoria nuevamente, causando comportamiento indefinido. Rust previene esto al requerir que todos los campos de un tipo Copy también sean Copy, asegurando recursivamente que no existan implementaciones de Drop anidadas.
¿mem::forget previene los problemas con Copy y Drop?
std::mem::forget consume un valor sin ejecutar su destructor, pero solo afecta a un valor específico poseído, no a todas sus copias. Si se permitieran Copy y Drop, olvidar una copia no evitaría que otras copias bit a bit ejecuten sus implementaciones de Drop cuando salgan de alcance. Esas eliminaciones restantes aún intentarían liberar el mismo recurso subyacente, lo que llevaría a un use-after-free o double-free sin importar la instancia olvidada.
¿Puedo usar ManuallyDrop para implementar Copy de manera segura?
Envolver un campo en ManuallyDrop previene la invocación automática de Drop, lo que técnicamente permite que la estructura externa derive Copy. Sin embargo, esto transfiere la responsabilidad de llamar a ManuallyDrop::drop al usuario para cada copia creada, creando efectivamente un escenario de gestión manual de memoria. Si el usuario olvida eliminar incluso una copia, el recurso se fuga permanentemente; Rust prohíbe este patrón para tipos que poseen recursos porque socava la garantía de seguridad de limpieza automática y determinista.