¿Qué es un "puntero gordo" en Rust?

91

Ya he leído el término "puntero gordo" en varios contextos, pero no estoy seguro de qué significa exactamente y cuándo se usa en Rust. El puntero parece ser dos veces más grande que un puntero normal, pero no entiendo por qué. También parece tener algo que ver con los objetos de rasgo.

Lukas Kalbertodt
fuente
7
El término en sí no es específico de Rust, por cierto. El puntero gordo generalmente se refiere a un puntero que almacena algunos datos adicionales además de la dirección del objeto al que se apunta. Si el puntero contiene algunos bits de etiqueta y, dependiendo de esos bits de etiqueta, el puntero a veces no es un puntero en absoluto, se denomina representación de puntero etiquetado . (Por ejemplo, en muchas VM de Smalltalks, los punteros que terminan con 1 bit son en realidad números enteros de 31/63 bits, ya que los punteros están alineados con palabras y, por lo tanto, nunca terminan en 1.) La JVM HotSpot llama a sus punteros gordos OOP s (Object-Oriented Punteros).
Jörg W Mittag
1
Solo una sugerencia: cuando publico un par de preguntas y respuestas, normalmente escribo una pequeña nota explicando que es una pregunta de respuesta propia y por qué decidí publicarla. Eche un vistazo a la nota al pie de la pregunta aquí: stackoverflow.com/q/46147231/5768908
Gerardo Furtado
@GerardoFurtado Inicialmente publiqué un comentario aquí explicando exactamente eso. Pero fue eliminado ahora (no por mí). Pero sí, estoy de acuerdo, ¡a menudo una nota así es útil!
Lukas Kalbertodt

Respuestas:

102

El término "puntero gordo" se utiliza para referirse a referencias y punteros en bruto a tipos de tamaño dinámico (DST): cortes u objetos de rasgo. Un puntero grueso contiene un puntero más información que hace que el DST sea "completo" (por ejemplo, la longitud).

Los tipos más utilizados en Rust no son DST, pero tienen un tamaño fijo conocido en el momento de la compilación. Estos tipos implementan el Sizedrasgo . Incluso los tipos que administran un búfer de pila de tamaño dinámico (como Vec<T>) son Sizedcomo el compilador sabe el número exacto de bytes Vec<T>que ocupará una instancia en la pila. Actualmente, hay cuatro tipos diferentes de DST en Rust.


Rebanadas ( [T]y str)

El tipo [T](para cualquiera T) tiene un tamaño dinámico (también lo es el tipo especial de "segmento de cadena" str). Es por eso que normalmente solo lo ves como &[T]o &mut [T], es decir, detrás de una referencia. Esta referencia es un "puntero gordo". Vamos a revisar:

dbg!(size_of::<&u32>());
dbg!(size_of::<&[u32; 2]>());
dbg!(size_of::<&[u32]>());

Esto imprime (con algo de limpieza):

size_of::<&u32>()      = 8
size_of::<&[u32; 2]>() = 8
size_of::<&[u32]>()    = 16

Entonces vemos que una referencia a un tipo normal como u32tiene un tamaño de 8 bytes, al igual que una referencia a una matriz [u32; 2]. Esos dos tipos no son DST. Pero al igual [u32]que un DST, la referencia es dos veces mayor. En el caso de las rebanadas, los datos adicionales que "completan" el DST son simplemente la longitud. Entonces se podría decir que la representación de &[u32]es algo como esto:

struct SliceRef { 
    ptr: *const u32, 
    len: usize,
}

Objetos de rasgo ( dyn Trait)

Cuando se usan rasgos como objetos de rasgo (es decir, tipo borrado, despachado dinámicamente), estos objetos de rasgo son DST. Ejemplo:

trait Animal {
    fn speak(&self);
}

struct Cat;
impl Animal for Cat {
    fn speak(&self) {
        println!("meow");
    }
}

dbg!(size_of::<&Cat>());
dbg!(size_of::<&dyn Animal>());

Esto imprime (con algo de limpieza):

size_of::<&Cat>()        = 8
size_of::<&dyn Animal>() = 16

Nuevamente, &Catsolo tiene 8 bytes de tamaño porque Cates un tipo normal. Pero dyn Animales un objeto de rasgo y, por lo tanto, de tamaño dinámico. Como tal, &dyn Animaltiene un tamaño de 16 bytes.

En el caso de los objetos de rasgo, los datos adicionales que completan el DST son un puntero a la vtable (vptr). No puedo explicar completamente el concepto de vtables y vptrs aquí, pero se utilizan para llamar a la implementación del método correcto en este contexto de despacho virtual. La vtable es un dato estático que básicamente solo contiene un puntero de función para cada método. Con eso, una referencia a un objeto de rasgo se representa básicamente como:

struct TraitObjectRef {
    data_ptr: *const (),
    vptr: *const (),
}

(Esto es diferente de C ++, donde el vptr para clases abstractas se almacena dentro del objeto. Ambos enfoques tienen ventajas y desventajas).


DST personalizados

De hecho, es posible crear sus propias DST al tener una estructura donde el último campo es un DST. Sin embargo, esto es bastante raro. Un ejemplo destacado es std::path::Path.

Una referencia o un puntero al horario de verano personalizado también es un puntero grueso. Los datos adicionales dependen del tipo de DST dentro de la estructura.


Excepción: tipos externos

En RFC 1861 , extern typese introdujo la función. Los tipos externos también son DST, pero sus indicadores no son indicadores gordos. O más exactamente, como dice el RFC:

En Rust, los punteros a las DST llevan metadatos sobre el objeto al que se apunta. Para cadenas y rebanadas, esta es la longitud del búfer, para objetos de rasgo, es la vtable del objeto. Para tipos externos, los metadatos son simples (). Esto significa que un puntero a un tipo externo tiene el mismo tamaño que un usize(es decir, no es un "puntero gordo").

Pero si no está interactuando con una interfaz C, probablemente nunca tendrá que lidiar con estos tipos externos.




Arriba, hemos visto los tamaños para referencias inmutables. Los punteros gordos funcionan de la misma manera para referencias mutables, punteros crudos inmutables y punteros crudos mutables:

size_of::<&[u32]>()       = 16
size_of::<&mut [u32]>()   = 16
size_of::<*const [u32]>() = 16
size_of::<*mut [u32]>()   = 16
Lukas Kalbertodt
fuente