Historia. Los primeros días de Rust requerían que todos los tipos tuvieran un tamaño conocido de manera estática para garantizar la asignación en la pila y una semántica de valor eficiente. Cuando se introdujeron los tipos de tamaño dinámico (DST) como los slices [T] y los objetos de rasgo dyn Trait para soportar estructuras de datos flexibles, el lenguaje necesitaba un mecanismo para distinguir entre parámetros genéricos con tamaño y potencialmente sin tamaño sin romper el código existente. La sintaxis ?Sized fue adoptada como un límite "relajado", permitiendo que los genéricos opten explícitamente por no cumplir con el requisito predeterminado de Sized mientras se preserva el comportamiento ergonómico predeterminado para la mayoría de los casos de uso que no involucran datos sin tamaño.
El Problema. El límite implícito T: **Sized** crea una tensión fundamental: permite la manipulación de valores y cálculos de memoria en tiempo de compilación, pero impide que las funciones acepten tipos dyn Trait o slices directamente sin indirección. Esta restricción obliga a los desarrolladores a usar Box o referencias incluso cuando se desean semánticas de propiedad, complicando las APIs que buscan soportar tanto la polimorfismo estática como dinámica. Sin ?Sized, el código genérico no puede abstraer sobre tipos concretos y objetos polimórficos en tiempo de ejecución, lo que lleva a la necesidad de asignaciones de heap forzadas o interfaces duplicadas para variantes con y sin tamaño.
La Solución. El compilador resuelve esto al hacer cumplir que los tipos limitados por ?Sized solo pueden accederse a través de punteros gruesos: valores compuestos que contienen un puntero de datos y metadatos en tiempo de ejecución (longitud para slices, vtable para objetos de rasgo). Cuando un genérico especifica T: **?Sized**, el compilador prohíbe operaciones que requieren tamaños conocidos, como std::mem::size_of::<T>() o mover valores por valor, asegurando que todos los diseños de memoria permanezcan calculables en tiempo de compilación. Este diseño permite abstracciones de costo cero donde los tipos con tamaño usan punteros delgados y los tipos sin tamaño usan punteros gruesos, con el sistema de tipos manejando la distinción de manera transparente.
Una biblioteca de monitoreo de sistemas necesitaba registrar errores que podían ser códigos de error pequeños, asignados en la pila, o grandes mensajes de error dinámicamente formateados que implementaran dyn **Display**. El diseño inicial de la API utilizando fn log<T: **Display**>(error: T) rechazaba objetos de rasgo porque el límite implícito de Sized impedía que dyn Display satisficiera la restricción, creando un obstáculo ergonómico significativo para el manejo dinámico de errores.
El primer enfoque considerado fue exigir Box<dyn **Display**> para todos los tipos de error, convirtiendo incluso códigos de error simples u32 en asignaciones de heap. Pros: Unificaba la superficie de la API y permitía la propiedad de errores dinámicos sin genéricos complejos. Contras: Introducía dependencias de asignador no adecuadas para objetivos embebidos y añadía latencia medible a los caminos calientes que manejan errores simples y estáticos.
La segunda opción consistió en mantener dos métodos de registro separados: uno para tipos con tamaño genérico T: **Display** y otro específicamente para &dyn **Display**. Pros: Evitó asignaciones de heap para tipos con tamaño y soportó correctamente el despacho dinámico para errores complejos. Contras: Requirió una significativa duplicación de código, complicó la documentación de la API pública y obligó a los llamadores a elegir el método correcto basado en el conocimiento previo del tamaño del tipo.
El equipo eligió un tercer enfoque utilizando fn log<T: **?Sized** + **Display**>(error: &T), aceptando referencias tanto a tipos con tamaño como sin tamaño. Esta solución fue elegida porque mantenía un único punto de entrada API coherente, soportaba entornos no-std al evitar el uso obligatorio de Box, y no imponía sobrecarga de tiempo de ejecución en comparación con el enfoque de dualidad de métodos. La implementación genérica se compiló a código máquina idéntico para los tipos con tamaño como la versión monomórfica original, mientras manejaba correctamente los objetos de rasgo a través del despacho de vtable.
El crate resultante se desplegó con éxito en microcontroladores y servidores, procesando millones de eventos de error heterogéneos sin sobrecarga de asignación. La interfaz unificada permitió a los desarrolladores pasar tanto &ConcreteError como &dyn Error sin problemas, demostrando que ?Sized permite un polimorfismo verdaderamente sin costo en una variedad de objetivos de despliegue.
¿Por qué no puede una función devolver un valor de tipo T donde T: **?Sized**?
Las funciones que devuelven valores deben colocar esos valores en registros o en la pila, requiriendo un tamaño conocido en tiempo de compilación para generar el código de la convención de llamadas correcta y reservar el espacio de pila adecuado. Dado que los tipos ?Sized como [i32] o dyn **Debug** tienen tamaños determinados en tiempo de ejecución, el compilador no puede generar las secuencias de instrucción de retorno de tamaño fijo necesarias para la ABI. Solo los tipos de puntero (Box<T>, &T) tienen tamaños conocidos de forma estática (ancho de usize o puntero grueso), lo que los convierte en los únicos tipos de retorno legales para datos sin tamaño, restringiendo fundamentalmente los genéricos ?Sized a tipos "vista" en lugar de tipos "valor" que pueden ser movidos por valor.
¿Cómo interactúa **?Sized** con las reglas de coherencia respecto a las implementaciones de rasgos para referencias?
Al implementar rasgos para &T donde T: **?Sized**, la implementación se aplica automáticamente a punteros gruesos (como &[i32] o &dyn Trait) porque son simplemente referencias a tipos ?Sized. Los candidatos a menudo pasan por alto que impl Trait for &T where T: **?Sized** cubre tanto punteros delgados como gruesos, mientras que impl Trait for T where T: **Sized** no lo hace. Esta distinción es crucial para definir implementaciones generales que funcionen tanto con datos con tamaño como con objetos de rasgo, asegurando coherencia en toda la jerarquía de tipos sin implementaciones superpuestas que violarían las reglas de huérfano de Rust.
¿Qué distingue la representación de memoria de **Box<dyn Trait>** de **&dyn Trait** más allá de las semánticas de propiedad?
Mientras que ambos usan punteros gruesos (puntero + vtable), **Box<dyn Trait>** posee la asignación y almacena el puntero vtable específicamente para fines de desasignación, mientras que **&dyn Trait** simplemente observa los datos. Crucialmente, Box<T> donde T: **?Sized** requiere que el asignador maneje la desasignación de tamaños dinámicos utilizando el tamaño almacenado en la vtable, mientras que las referencias no tienen tal responsabilidad. Los principiantes a menudo pasan por alto que Box permite la asignación en heap de tipos sin tamaño que no pueden existir en la pila, mientras que las referencias simplemente toman prestada memoria existente, haciendo que Box sea esencial para devolver datos sin tamaño que son propiedad de las funciones.