Bisher hatten wir mit Ownership, References und Lifetimes ein klares Modell: jeder Wert hat genau einen Besitzer, geliehene Referenzen müssen die exklusive-mutable-XOR-shared-immutable-Regel einhalten, und alles wird statisch geprüft. Dieses Modell deckt erstaunlich viel ab — aber nicht alles. Manche Datenstrukturen brauchen geteiltes Eigentum (ein Knoten in einem Graph wird von mehreren Stellen referenziert), manche brauchen Mutation hinter geteilten Referenzen (ein Cache, der innerhalb eines &self-Aufrufs Einträge nachladen kann), manche brauchen Heap-Allokation mit unbekannter Größe (eine rekursive Datenstruktur). Für all diese Fälle gibt es Smart Pointers: Wrapper-Typen aus der Stdlib, die das Ownership-Modell punktuell erweitern und dabei selbst sicher bleiben.
Was ein Smart Pointer ist
Ein Smart Pointer ist ein Typ, der sich wie eine Referenz verhält, aber zusätzlich Verantwortung trägt: für die Allokation, für Reference-Counting, für die Synchronisation, für das Drop. Anders gesagt: ein Smart Pointer ist eine Datenstruktur, die einen Wert hält und über den Deref-Trait (und meist auch DerefMut) so tut, als wäre sie eine Referenz auf den inneren Wert.
In der Praxis sieht das so aus:
let b: Box<i32> = Box::new(42); // Box hält i32 auf dem Heap
let n: i32 = *b; // Deref: greife auf den Wert zu
println!("{n}"); // 42Box<i32> ist ein Smart Pointer. Er ist kein roher Pointer wie in C — er ist ein Stdlib-Typ mit Methoden, mit Drop-Logik (die den Heap-Speicher freigibt) und mit Deref-Impl (damit *b funktioniert). Genau dasselbe Prinzip gilt für Rc, Arc, RefCell, Mutex und alle anderen Smart Pointers — sie variieren nur in ihren Garantien.
Auch Vec<T> und String sind technisch Smart Pointers (heap-allozierter Speicher, Drop-Logik, Deref). Aus didaktischen Gründen behandeln wir sie nicht hier — sie haben ihr eigenes Kapitel —, aber es lohnt sich zu wissen, dass das gleiche Pattern dahintersteckt.
Die zwei Themen: Heap-Allokation und Interior Mutability
Smart Pointers lösen in der Stdlib zwei klar getrennte Probleme.
Heap-Allokation und geteiltes Eigentum
Manche Werte brauchen einen festen Speicher-Ort, der unabhängig vom Stack-Frame des Aufrufers existiert. Klassische Gründe: rekursive Datenstrukturen, deren Größe der Compiler nicht zur Compile-Zeit kennen kann; Werte, die viele Owner haben sollen; Werte, die zwischen Threads geteilt werden.
Dafür gibt es drei Hauptwerkzeuge:
Box<T>— der einfache Heap-Container. Genau ein Owner, der den Wert beim Drop freigibt.Rc<T>— Reference-Counting, single-threaded. Mehrere Owner zählen Klone, der letzte gibt den Wert frei.Arc<T>— Reference-Counting, multi-threaded. WieRc, aber mit atomaren Operationen für Thread-Sicherheit.
Interior Mutability
Die zweite Klasse löst ein anderes Problem: was tun, wenn du einen Wert hinter einer geteilten Referenz modifizieren musst? Die Borrow-Regeln verbieten das — geteilte Referenzen erlauben kein Schreiben. Aber manche Patterns brauchen genau das: ein Logger, der innerhalb von &self-Methoden Zähler hochzählt; ein Cache, der bei &self-Lookups neue Einträge anlegt; ein Observer, der eine Liste von Callbacks pflegt.
Smart Pointers mit Interior Mutability verschieben die Borrow-Prüfung von der Compile-Zeit zur Laufzeit (oder umgehen sie ganz, mit Sicherheits-Mechanismen darum herum):
Cell<T>— Mutation per Wert-Tausch. Nur fürCopy-Typen sinnvoll, sehr leichtgewichtig.RefCell<T>— Mutation per Runtime-Borrow-Check. Bei Verletzung der Regeln panickt das Programm.Mutex<T>— Mutation mit blockierendem Lock. Thread-sicher.RwLock<T>— Mutation mit Lese-/Schreib-Lock. Thread-sicher, mehrere Reader gleichzeitig.
Der Entscheidungs-Baum
Eine kompakte Heuristik, die fast immer reicht:
| Frage | Antwort | Werkzeug |
|---|---|---|
| Brauche ich Heap-Allokation für genau einen Owner? | Ja | Box<T> |
| Brauche ich mehrere Owner für denselben Wert? | Ja, single-threaded | Rc<T> |
| Brauche ich mehrere Owner über Threads? | Ja | Arc<T> |
Brauche ich Mutation hinter &self ohne Lock? | Ja, Copy-Wert | Cell<T> |
Brauche ich Mutation hinter &self ohne Lock? | Ja, non-Copy | RefCell<T> |
| Brauche ich Mutation über Threads, blockierend? | Ja | Mutex<T> |
| Brauche ich Mutation über Threads, mit Reader-Mehrheit? | Ja | RwLock<T> |
| Will ich Copy-on-Write zwischen Borrow und Owned? | Ja | Cow<'a, T> |
In der Praxis kommen Kombinationen vor: Rc<RefCell<T>> für single-threaded shared mutable state, Arc<Mutex<T>> für die thread-safe Variante. Das sind die häufigsten Compound-Pattern und bekommen in diesem Kapitel eigene Artikel.
Heap vs. Stack — kurze Erinnerung
Im Ownership-Kapitel haben wir Stack und Heap unterschieden: der Stack hält fest-große Werte, deren Lebensdauer an einen Funktions-Frame gebunden ist; der Heap hält Werte mit dynamischer Größe oder Lebensdauer. Smart Pointers wie Box, Rc, Arc liegen alle auf dem Stack (oder eingebettet in andere Structs), aber sie zeigen auf Speicher auf dem Heap.
let b: Box<i32> = Box::new(42);
// Stack: b ist ein 8-Byte-Pointer
// Heap: irgendwo liegt der Wert 42 (4 Bytes + Allocator-Overhead)Das ist wichtig zu wissen, weil es die Performance-Charakteristik prägt: ein Funktions-Aufruf, der einen Box<T> zurückgibt, kostet die Heap-Allocation. Eine Funktion, die einen rohen T zurückgibt (sofern groß genug, dass es als Move-Operation eingesetzt wird), kostet nur eine Stack-Bewegung. Bei Hot-Paths achten Performance-bewusste Bibliotheken darauf.
Interior Mutability — der semantische Trick
Interior Mutability klingt wie ein Bruch der Borrow-Regeln, ist aber keiner. Die Regel heißt: wenn der Compiler nicht beweisen kann, dass die Mutation sicher ist, dann sorgt der Smart Pointer dafür. RefCell etwa führt zur Laufzeit Buch, wie viele Borrows gerade aktiv sind, und panickt, wenn die XOR-Regel verletzt würde. Mutex blockiert, bis kein anderer Thread mehr schreibt.
Konkret heißt das: die &self-Methode bekommt nach außen weiterhin eine geteilte Referenz, intern aber darf sie über den Smart Pointer mutieren — die Sicherheit garantiert die Datenstruktur, nicht der Compiler. Diese Verschiebung von Compile-Zeit zu Laufzeit ist der Preis, den du für die zusätzliche Flexibilität zahlst.
use std::cell::RefCell;
struct Logger {
count: RefCell<u32>, // RefCell erlaubt Mutation über &self
}
impl Logger {
fn log(&self, msg: &str) { // &self, nicht &mut self!
*self.count.borrow_mut() += 1;
println!("[{}] {msg}", self.count.borrow());
}
}
fn main() {
let logger = Logger { count: RefCell::new(0) };
logger.log("first");
logger.log("second"); // count = 2
}Ohne RefCell wäre das nicht möglich: &self.count += 1 würde der Compiler ablehnen, weil &self keine Mutation erlaubt. Mit RefCell ist der Counter innerhalb des Smart Pointers, der Borrow-Check für die innere Mutation läuft zur Laufzeit, und nach außen sieht alles harmlos aus.
Wie Smart Pointers transparent wirken — Deref
Ein zentraler Mechanismus, der Smart Pointers ergonomisch macht, ist Deref-Coercion: der Compiler kann automatisch von &Smart<T> zu &T konvertieren, sodass du Methoden des inneren Typs direkt auf dem Wrapper aufrufen kannst.
let b: Box<String> = Box::new(String::from("hello"));
// Wir können String-Methoden direkt aufrufen:
let len = b.len(); // Box → &String → &str via Deref-Kette
println!("{len}"); // 5
// Wir können Box<String> als &str übergeben:
fn ausgabe(s: &str) { println!("{s}"); }
ausgabe(&b); // Box<String> → &String → &strDiese Mechanik ist nicht magisch — sie ist eine Anwendung des Deref-Traits, der pro Smart-Pointer-Typ implementiert ist. Ein eigener Artikel in diesem Kapitel behandelt die Coercion-Kette im Detail.
Was dieses Kapitel abdeckt
Die folgenden Artikel gehen in dieser Reihenfolge durch:
- Box — der grundlegende Heap-Container. Rekursive Strukturen, Trait-Objekte, der zero-cost Anteil von Smart Pointers.
- Rc — Reference-Counting für mehrere Owner, single-threaded.
- Arc — Reference-Counting mit Atomic-Operationen, thread-safe. Wann Arc, wann Rc.
- RefCell und Cell — Interior Mutability für single-threaded Code. Borrow-Tracking zur Laufzeit, die XOR-Regel als Runtime-Check.
- Mutex — Blockierender Lock für thread-safe Mutation. Poison-Mechanik, MutexGuard, Deadlock-Vermeidung.
- RwLock — Reader-Writer-Lock für Szenarien mit vielen Lesern. Wann lohnt sich der zusätzliche Aufwand.
- Cow — Copy-on-Write. Borrowed oder Owned je nach Bedarf, ideal für „selten mutierte" Daten.
- Rc-RefCell-Pattern und Arc-Mutex-Pattern — die zwei wichtigsten Compound-Pattern in der Praxis.
- Deref-Coercion — wie der Compiler aus Smart-Pointer-Wrappern Method-Calls auf inneren Typen ableitet, was die Coercion-Kette erlaubt und was nicht.
Interessantes
Smart Pointer = Wrapper mit Drop + Deref.
Stdlib-Typ, der einen Wert hält, ihn beim Drop sauber aufräumt und durch Deref so wirkt, als wäre er eine Referenz auf den inneren Wert. Box, Rc, Arc, RefCell, Mutex, RwLock, Cow.
Zwei Themen: Heap-Allokation und Interior Mutability.
Box/Rc/Arc lösen das Speicher- und Owner-Thema. Cell/RefCell/Mutex/RwLock lösen das Mutations-Thema. Cow steht quer dazu und löst das „borrowed oder owned"-Thema.
Entscheidungs-Heuristik: Frage, Antwort, Werkzeug.
Mehrere Owner? Rc oder Arc. Mutation hinter &self? Cell, RefCell oder Mutex/RwLock. Beides? Rc<RefCell> (single-threaded) oder Arc<Mutex> (threads).
Rc und Arc sind nicht schneller als Box.
Sie sind oft langsamer (Reference-Count-Operationen, bei Arc atomic). Nutze sie nur, wenn du wirklich mehrere Owner brauchst. Single-Owner-Daten gehören in Box<T> oder direkt in Stack-Strukturen.
Interior Mutability verschiebt die Prüfung zur Laufzeit.
Compile-Zeit-Garantien wären zu strikt. RefCell, Mutex, RwLock führen die Borrow-Logik zur Laufzeit. Preis: möglicher Panic (RefCell) oder möglicher Deadlock (Mutex).
Deref-Coercion macht Smart Pointers ergonomisch.
Methoden des inneren Typs werden auf dem Wrapper direkt aufrufbar. Box<String>::len() ruft String::len() über die Deref-Kette Box<String> → &String → &str (zwei Steps). Mehr im Deref-Coercion-Artikel.
Compound-Pattern: Rc> und Arc>.
Geteiltes Eigentum + Interior Mutability in einem. Single-threaded mit Rc+RefCell, multi-threaded mit Arc+Mutex (oder Arc+RwLock). Eigene Artikel in diesem Kapitel.
Smart Pointers ersetzen NICHT die Borrow-Regeln.
Sie ergänzen sie für Fälle, die rein statisch zu strikt wären. In den meisten Anwendungs-Bereichen kommst du ohne aus. Wer Rc<RefCell<T>> überall hat, hat oft einen Design-Fehler — der Compiler will dir etwas sagen.
Weiterführende Ressourcen
Externe Quellen
- The Rust Book – Smart Pointers
- The Rust Book – Reference Cycles and Reference Counting
- Rust Reference – Pointer Types
- Common Rust Lifetime Misconceptions