pointers - ¿Qué es un "puntero gordo" en Rust?
(1)
El término "puntero gordo" se utiliza para referirse a referencias y punteros sin formato a tipos de tamaño dinámico (DST): cortes u objetos de rasgos. 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 tiempo de compilación.
Estos tipos implementan
el rasgo de
Sized
.
Incluso los tipos que administran un búfer de almacenamiento dinámico de tamaño dinámico (como
Vec<T>
) se
Sized
ya que el compilador sabe el número exacto de bytes que una instancia de
Vec<T>
ocupará en la pila.
Actualmente hay cuatro tipos diferentes de DST en Rust.
Rebanadas (
[T]
y
str
)
El tipo
[T]
(para cualquier
T
) tiene un tamaño dinámico (también lo es el tipo especial "string slice"
str
).
Es por eso que generalmente solo lo ves como
&[T]
o
&mut [T]
, es decir, detrás de una referencia.
Esta referencia es un llamado "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
u32
tiene 8 bytes de tamaño, como lo es una referencia a una matriz
[u32; 2]
[u32; 2]
Esos dos tipos no son DST.
Pero como
[u32]
es un DST, la referencia a él es el doble de grande.
En el caso de los cortes, los datos adicionales que "completan" el horario de verano 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 rasgos (es decir, tipo borrado, despachado dinámicamente), estos objetos de rasgos 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,
&Cat
tiene solo 8 bytes porque
Cat
es un tipo normal.
Pero
dyn Animal
es un objeto de rasgo y, por lo tanto, de tamaño dinámico.
Como tal,
&dyn Animal
tiene 16 bytes de longitud.
En el caso de los objetos de rasgos, los datos adicionales que completan el DST es un puntero a la tabla vtable (la vptr). No puedo explicar completamente el concepto de vtables y vptrs aquí, pero se utilizan para llamar a la implementación correcta del método en este contexto de despacho virtual. El 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 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
En realidad, es posible crear sus propios 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 puntero al DST 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
, se introdujo la característica de
extern type
.
Los tipos externos también son DST, pero los punteros a ellos
no
son punteros gordos.
O más exactamente, como dice el RFC:
En Rust, los punteros a los DST llevan metadatos sobre el objeto al que se apunta. Para cadenas y cortes, esta es la longitud del búfer, para los objetos de rasgos, esta es la tabla vtable del objeto. Para los tipos externos, los metadatos son simplemente
()
. Esto significa que un puntero a un tipo externo tiene el mismo tamaño que unusize
(es decir, no es un "puntero grueso").
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 igual 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
Ya leí el término "indicador 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 objetos de rasgos.