Was ist ein „fetter Zeiger“ in Rust?

91

Ich habe den Begriff "Fettzeiger" bereits in mehreren Zusammenhängen gelesen, bin mir aber nicht sicher, was er genau bedeutet und wann er in Rust verwendet wird. Der Zeiger scheint doppelt so groß zu sein wie ein normaler Zeiger, aber ich verstehe nicht warum. Es scheint auch etwas mit Merkmalsobjekten zu tun zu haben.

Lukas Kalbertodt
quelle
7
Der Begriff selbst ist übrigens nicht rostspezifisch. Fettzeiger bezieht sich im Allgemeinen auf einen Zeiger, der neben der Adresse des Objekts, auf das verwiesen wird, einige zusätzliche Daten speichert. Wenn der Zeiger einige Tag-Bits enthält und abhängig von diesen Tag-Bits der Zeiger manchmal überhaupt kein Zeiger ist, wird er als Tag-Zeiger-Darstellung bezeichnet . (ZB auf vielen Smalltalks-VMs sind Zeiger, die mit einem 1-Bit enden, tatsächlich 31/63-Bit-Ganzzahlen, da Zeiger wortausgerichtet sind und daher niemals mit 1 enden.) Die HotSpot-JVM nennt ihre fetten Zeiger OOPs (Object-Oriented) Zeiger).
Jörg W Mittag
1
Nur ein Vorschlag: Wenn ich ein Q & A-Paar poste, schreibe ich normalerweise eine kleine Notiz, in der erklärt wird, dass es sich um eine selbst beantwortete Frage handelt und warum ich mich dazu entschlossen habe, sie zu posten. Schauen Sie sich die Fußnote in der Frage hier an: stackoverflow.com/q/46147231/5768908
Gerardo Furtado
@GerardoFurtado Ich habe hier zunächst einen Kommentar gepostet, der genau das erklärt. Aber es wurde jetzt entfernt (nicht von mir). Aber ja, ich stimme zu, oft ist eine solche Notiz nützlich!
Lukas Kalbertodt
@ Jörg W Mittag 'gewöhnliche Objektzeiger', in der Tat
Obataku

Antworten:

102

Der Begriff "Fettzeiger" bezieht sich auf Referenzen und Rohzeiger auf Typen mit dynamischer Größe (DSTs) - Slices oder Trait-Objekte. Ein fetter Zeiger enthält einen Zeiger sowie einige Informationen, die die Sommerzeit "vollständig" machen (z. B. die Länge).

Die am häufigsten verwendeten Typen in Rust sind keine Sommerzeiten, sondern haben eine feste Größe, die zur Kompilierungszeit bekannt ist. Diese Typen implementieren das SizedMerkmal . Selbst Typen, die einen Heap-Puffer mit dynamischer Größe (wie Vec<T>) verwalten, Sizedwissen genau, wie viele Bytes eine Vec<T>Instanz auf dem Stapel benötigt. Derzeit gibt es in Rust vier verschiedene Arten von Sommerzeiten.


Scheiben ( [T]und str)

Der Typ [T](für jeden T) hat eine dynamische Größe (ebenso der spezielle Typ "String Slice" str). Deshalb sehen Sie es normalerweise nur als &[T]oder &mut [T], dh hinter einer Referenz. Diese Referenz ist ein sogenannter "Fettzeiger". Lass uns nachsehen:

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

Dies druckt (mit einigen Aufräumarbeiten):

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

Wir sehen also, dass ein Verweis auf einen normalen Typ wie u328 Byte groß ist, ebenso wie ein Verweis auf ein Array [u32; 2]. Diese beiden Typen sind keine Sommerzeiten. Aber wie [u32]bei einer Sommerzeit ist der Verweis darauf doppelt so groß. Im Fall von Slices sind die zusätzlichen Daten, die die Sommerzeit "vervollständigen", einfach die Länge. Man könnte also sagen, dass die Darstellung &[u32]ungefähr so ​​ist:

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

Merkmalsobjekte ( dyn Trait)

Bei Verwendung von Merkmalen als Merkmalsobjekte (dh Typ gelöscht, dynamisch versendet) sind diese Merkmalsobjekte Sommerzeiten. Beispiel:

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

Dies druckt (mit einigen Aufräumarbeiten):

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

Auch hier &Catist nur 8 Bytes groß, da Cates sich um einen normalen Typ handelt. Ist dyn Animalaber ein Merkmalsobjekt und daher dynamisch dimensioniert. Als solches &dyn Animalist 16 Bytes groß.

Bei Merkmalsobjekten sind die zusätzlichen Daten, die die Sommerzeit vervollständigen, ein Zeiger auf die vtable (die vptr). Ich kann das Konzept von vtables und vptrs hier nicht vollständig erklären, aber sie werden verwendet, um die korrekte Methodenimplementierung in diesem virtuellen Versandkontext aufzurufen. Die vtable ist ein statisches Datenelement, das im Grunde nur einen Funktionszeiger für jede Methode enthält. Damit wird ein Verweis auf ein Merkmalsobjekt grundsätzlich dargestellt als:

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

(Dies unterscheidet sich von C ++, wo das vptr für abstrakte Klassen im Objekt gespeichert ist. Beide Ansätze haben Vor- und Nachteile.)


Benutzerdefinierte Sommerzeiten

Es ist tatsächlich möglich, eigene Sommerzeiten zu erstellen, indem Sie eine Struktur haben, in der das letzte Feld eine Sommerzeit ist. Dies ist jedoch eher selten. Ein prominentes Beispiel ist std::path::Path.

Ein Verweis oder Zeiger auf die benutzerdefinierte Sommerzeit ist auch ein fetter Zeiger. Die zusätzlichen Daten hängen von der Art der Sommerzeit innerhalb der Struktur ab.


Ausnahme: Externe Typen

In RFC 1861 wurde die extern typeFunktion eingeführt. Externe Typen sind ebenfalls Sommerzeiten, aber Zeiger auf sie sind keine fetten Zeiger. Oder genauer gesagt, wie der RFC es ausdrückt:

In Rust enthalten Zeiger auf Sommerzeiten Metadaten zu dem Objekt, auf das verwiesen wird. Für Strings und Slices ist dies die Länge des Puffers, für Trait-Objekte die vtable des Objekts. Für externe Typen sind die Metadaten einfach (). Dies bedeutet, dass ein Zeiger auf einen externen Typ dieselbe Größe wie a hat usize(dh es ist kein "fetter Zeiger").

Wenn Sie jedoch nicht mit einer C-Schnittstelle interagieren, müssen Sie sich wahrscheinlich nie mit diesen externen Typen befassen.




Oben haben wir die Größen für unveränderliche Referenzen gesehen. Fette Zeiger funktionieren genauso für veränderbare Referenzen, unveränderliche Rohzeiger und veränderbare Rohzeiger:

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