Cos'è un "fat pointer"?


95

Ho già letto il termine "fat pointer" in diversi contesti, ma non sono sicuro di cosa significhi esattamente e quando sia usato in Rust. Il puntatore sembra essere due volte più grande di un normale puntatore, ma non capisco perché. Sembra anche avere qualcosa a che fare con gli oggetti tratto.


7
Il termine stesso non è specifico di Rust, BTW. Il fat pointer si riferisce generalmente a un puntatore che memorizza alcuni dati extra oltre al solo indirizzo dell'oggetto a cui si punta. Se il puntatore contiene alcuni bit di tag e, a seconda di questi bit di tag, il puntatore a volte non è affatto un puntatore, è chiamato rappresentazione del puntatore con tag . (Ad esempio, su molte VM Smalltalks, i puntatori che terminano con 1 bit sono in realtà interi a 31/63 bit, poiché i puntatori sono allineati a parole e quindi non finiscono mai in 1.) La JVM HotSpot chiama i suoi puntatori fat OOP s (Object-Oriented Puntatori).
Jörg W Mittag

2
Solo un suggerimento: quando pubblico una coppia di domande e risposte normalmente scrivo una piccola nota spiegando che si tratta di una domanda a risposta personale e perché ho deciso di pubblicarla. Dai un'occhiata alla nota a piè di pagina nella domanda qui: stackoverflow.com/q/46147231/5768908
Gerardo Furtado

@GerardoFurtado inizialmente ho pubblicato un commento qui spiegando esattamente questo. Ma ora è stato rimosso (non da me). Ma sì, sono d'accordo, spesso una nota del genere è utile!
Lukas Kalbertodt

Risposte:


110

Il termine "puntatore grasso" viene utilizzato per fare riferimento a riferimenti e puntatori non elaborati a tipi di dimensioni dinamiche (DST): sezioni o oggetti tratto. Un puntatore grasso contiene un puntatore più alcune informazioni che rendono il DST "completo" (ad esempio la lunghezza).

I tipi più comunemente usati in Rust non sono DST, ma hanno una dimensione fissa nota al momento della compilazione. Questi tipi implementano il Sizedtratto . Anche i tipi che gestiscono un buffer di heap di dimensioni dinamiche (come Vec<T>) sono Sizedpoiché il compilatore conosce il numero esatto di byte che Vec<T>un'istanza occuperà nello stack. Attualmente ci sono quattro diversi tipi di DST in Rust.


Fette ( [T]e str)

Il tipo [T](per any T) è dimensionato dinamicamente (così è il tipo speciale "string slice" str). Ecco perché di solito lo vedi solo come &[T]o &mut [T], cioè dietro un riferimento. Questo riferimento è un cosiddetto "fat pointer". Controlliamo:

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

Questo stampa (con un po 'di pulizia):

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

Quindi vediamo che un riferimento a un tipo normale come u32è grande 8 byte, così come un riferimento a un array [u32; 2]. Questi due tipi non sono DST. Ma poiché [u32]è un DST, il riferimento ad esso è due volte più grande. Nel caso delle sezioni, i dati aggiuntivi che "completano" l'ora legale sono semplicemente la lunghezza. Quindi si potrebbe dire che la rappresentazione di &[u32]è qualcosa del genere:

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

Oggetti tratto ( dyn Trait)

Quando si usano i tratti come oggetti tratto (cioè il tipo cancellato, inviato dinamicamente), questi oggetti tratto sono DST. Esempio:

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>());

Questo stampa (con un po 'di pulizia):

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

Anche in questo caso, &Catè grande solo 8 byte perché Catè un tipo normale. Ma dyn Animalè un oggetto tratto e quindi dimensionato dinamicamente. In quanto tale, &dyn Animalè grande 16 byte.

Nel caso di oggetti tratto, i dati aggiuntivi che completano il DST sono un puntatore a vtable (il vptr). Non posso spiegare completamente il concetto di vtables e vptrs qui, ma sono usati per chiamare l'implementazione corretta del metodo in questo contesto di invio virtuale. Il vtable è un pezzo di dati statico che fondamentalmente contiene solo un puntatore a funzione per ogni metodo. Con ciò, un riferimento a un oggetto tratto è fondamentalmente rappresentato come:

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

(Questo è diverso da C ++, dove il vptr per le classi astratte è memorizzato all'interno dell'oggetto. Entrambi gli approcci hanno vantaggi e svantaggi.)


DST personalizzati

In realtà è possibile creare i propri DST avendo una struttura in cui l'ultimo campo è un DST. Questo è piuttosto raro, però. Un esempio importante è std::path::Path.

Un riferimento o un puntatore all'ora legale personalizzata è anche un puntatore grasso. I dati aggiuntivi dipendono dal tipo di DST all'interno della struttura.


Eccezione: tipi esterni

In RFC 1861 , la extern typefunzionalità è stata introdotta. Anche i tipi esterni sono DST, ma i puntatori ad essi non sono puntatori fat. O più esattamente, come dice la RFC:

In Rust, i puntatori ai DST trasportano metadati sull'oggetto a cui si punta. Per stringhe e slice questa è la lunghezza del buffer, per gli oggetti trait questa è la vtable dell'oggetto. Per i tipi esterni i metadati sono semplicemente (). Ciò significa che un puntatore a un tipo esterno ha la stessa dimensione di a usize(cioè non è un "puntatore grasso").

Ma se non stai interagendo con un'interfaccia C, probabilmente non dovrai mai avere a che fare con questi tipi esterni.




Sopra, abbiamo visto le dimensioni per i riferimenti immutabili. I puntatori grassi funzionano allo stesso modo per riferimenti mutabili, puntatori grezzi immutabili e puntatori grezzi mutabili:

size_of::<&[u32]>()       = 16
size_of::<&mut [u32]>()   = 16
size_of::<*const [u32]>() = 16
size_of::<*mut [u32]>()   = 16
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.