Box<T> ist der einfachste Smart Pointer der Stdlib und gleichzeitig der wichtigste Lehrtyp: alles, was bei größeren Smart Pointers wie Rc oder Arc dazukommt, baut auf demselben Grundprinzip auf, das Box etabliert. Eine Box<T> hält einen Wert vom Typ T auf dem Heap, der Wrapper selbst lebt auf dem Stack (oder eingebettet in andere Strukturen), und beim Drop wird der Heap-Speicher automatisch freigegeben. Die zentralen Anwendungen — rekursive Datenstrukturen und Trait-Objekte — wären ohne Box in Rust gar nicht möglich. Dieser Artikel zeigt jeden Aspekt im Detail.

Warum überhaupt eine Box?

Bevor wir die Syntax anschauen, klären wir das Problem, das Box löst. In Rust liegt ein normaler Wert standardmäßig auf dem Stack — dem Speicher-Bereich, der zum aktuellen Funktions-Aufruf gehört. Stack-Werte sind extrem schnell (Allokation kostet keine Zeit, nur Stack-Pointer-Verschiebung) und werden automatisch freigegeben, wenn die Funktion zurückkehrt.

Das funktioniert wunderbar, solange drei Bedingungen gelten:

  1. Die Größe des Werts ist zur Compile-Zeit bekannt.
  2. Der Wert lebt nicht länger als der Funktions-Aufruf.
  3. Der Wert ist klein genug, dass er den Stack nicht überlaufen lässt.

Sobald eine dieser Bedingungen verletzt ist, kommt der Heap ins Spiel — ein separater, viel größerer Speicher-Bereich mit dynamischer Lebenszeit. In C alloziert man dort manuell mit malloc und gibt mit free zurück. In Java/Python kümmert sich der Garbage Collector. In Rust ist der häufigste Weg: Box::new(...).

Eine Box<T> ist ein Owner für Heap-Speicher: sie reserviert Platz, schreibt den Wert dorthin, behält einen Pointer, und gibt den Speicher beim Drop wieder frei. Du arbeitest mit der Box wie mit einem normalen Wert; Rust sorgt im Hintergrund für die Allokation und das Aufräumen.

Konstruktion und Grundverhalten

Eine Box legst du mit Box::new(wert) an. Beim Aufruf passieren drei Dinge:

  1. Auf dem Heap wird Platz für einen T reserviert.
  2. Der Wert wird vom Stack auf den Heap kopiert (bei Copy) oder gemoved (bei non-Copy).
  3. Die Box (8 Bytes auf 64-Bit-Systemen) hält den Heap-Pointer.
Rust Erste Box
fn main() {
    let b: Box<i32> = Box::new(42);
    println!("{b}");      // 42 — Display nutzt Deref-Coercion

    let n: i32 = *b;       // Explizite Dereferenzierung
    assert_eq!(n, 42);
}

Hier ist es lehrreich, die Zeilen einzeln durchzugehen:

let b: Box<i32> = Box::new(42); — der Wert 42 (Typ i32, 4 Bytes) wird auf dem Heap angelegt. Die Bindung b selbst lebt auf dem Stack und ist nur ein 8-Byte-Pointer, der dorthin zeigt.

println!("{b}") — das ist subtil: b ist eine Box<i32>, aber der Display-Trait, den println! für {} braucht, ist auf i32 implementiert, nicht auf Box<i32>. Es funktioniert trotzdem, weil Deref-Coercion (eigener Artikel) die Box automatisch zu einem &i32 macht. Der Print sieht also den inneren Wert.

let n: i32 = *b; — der *-Operator ist die explizite Dereferenzierung. Bei einer Box<i32> ist *b der i32-Wert selbst. Da i32 Copy ist, wird kopiert; bei einem non-Copy-Typ wie Box<String> würde *b den inneren String aus der Box herausziehen (Move) — danach wäre b leer und nicht mehr nutzbar.

Wichtig zur Klärung: Box ist kein Pointer-Typ im Sinne von „mehrere können auf denselben Wert zeigen". Sie hat genau einen Owner und folgt der ganz normalen Move-Semantik von Rust. Wenn du eine Box weitergibst, ist sie weg:

Rust Move bei Box
fn take(b: Box<String>) {
    println!("{b}");
}

fn main() {
    let s = Box::new(String::from("hello"));
    take(s);
    // println!("{s}");     // FEHLER: s wurde gemoved
}

Wenn du die kommentierte Zeile aktivierst, sagt der Compiler:

text rustc-Ausgabe
error[E0382]: borrow of moved value: `s`
  --> src/main.rs:7:21
   |
 6 |     take(s);
   |          - value moved here
 7 |     println!("{s}");
   |               ^ value borrowed here after move
   |
   = note: this error originates in the macro `println`

Die Diagnose zeigt genau die zwei kritischen Stellen: wo der Wert gemoved wurde („value moved here") und wo du danach versuchst, ihn zu nutzen („borrowed here after move"). Wer das einmal gesehen hat, erkennt das Muster überall. Box ändert nichts an dieser Logik — sie folgt den Standard-Ownership-Regeln.

Was Box konkret macht — Speicher-Layout

Eine Box<T> ist intern ein einzelner Pointer auf den Heap. Auf 64-Bit-Systemen sind das 8 Bytes — egal wie groß T ist. Dieser Satz ist so zentral, dass er ein paar Beispiele rechtfertigt.

Rust Größe und Layout
use std::mem::size_of;

fn main() {
    println!("size_of::<i32>()              = {}", size_of::<i32>());              // 4
    println!("size_of::<Box<i32>>()         = {}", size_of::<Box<i32>>());         // 8
    println!("size_of::<[u8; 1024]>()       = {}", size_of::<[u8; 1024]>());       // 1024
    println!("size_of::<Box<[u8; 1024]>>()  = {}", size_of::<Box<[u8; 1024]>>());  // 8
    println!("size_of::<[u8; 1_000_000]>()  = {}", size_of::<[u8; 1_000_000]>());  // 1_000_000
    println!("size_of::<Box<[u8; 1_000_000]>>() = {}", size_of::<Box<[u8; 1_000_000]>>()); // 8
}

Du siehst: das nackte [u8; 1024] braucht 1 KB Stack-Speicher, das nackte [u8; 1_000_000] braucht 1 MB. Aber jede Box<...> ist konstant 8 Bytes. Die eigentlichen Daten liegen weit weg auf dem Heap; im Stack-Frame steckt nur der Wegweiser dahin.

Eine kleine Schematische Darstellung hilft, das visuell festzunageln:

text Stack vs. Heap bei Box::new([0u8; 1024])
Stack-Frame der Funktion:                Heap (irgendwo):

  +----------------------+                +--------------------+
  | b: Box<[u8; 1024]>   | -- zeigt auf ->| [0, 0, 0, ..., 0]  |
  |   (8 Bytes Pointer)  |                |  1024 Bytes        |
  +----------------------+                +--------------------+

Diese Trennung ist die Grundlage von zwei mächtigen Eigenschaften:

  • Fixe Größe trotz variabler Daten: ein Struct, das eine Box<T> enthält, hat eine bekannte Größe, auch wenn T riesig oder zur Compile-Zeit unbekannt ist. Das wird gleich bei rekursiven Strukturen und Trait-Objekten zentral.
  • Übergabe ist günstig: einen Box<[u8; 1MB]> an eine Funktion zu übergeben kostet 8 Bytes Move (den Pointer), nicht 1 MB Kopie. Der Heap-Inhalt bleibt am selben Platz.

Hinter den Kulissen ist die Heap-Allokation natürlich nicht kostenlos — Box::new ruft den Allocator auf, der freien Heap-Speicher sucht. Auf modernen Systemen sind das wenige Nanosekunden, aber in einem extrem heißen Loop summiert sich das. Mehr dazu im Performance-Abschnitt weiter unten.

Anwendung 1: Rekursive Datenstrukturen

Eine rekursive Struktur enthält Felder, die wieder vom selben Typ sind. Klassiker: eine verkettete Liste (jeder Knoten zeigt auf den nächsten), ein Binärbaum (jeder Knoten hat zwei Kinder), ein abstrakter Syntaxbaum (jeder Ausdruck enthält Sub-Ausdrücke). In allen drei Fällen brauchen wir die Möglichkeit, denselben Typ innerhalb des Typs zu verwenden.

In Sprachen mit Garbage Collector ist das trivial: Java- oder Python-Klassen sind sowieso Heap-Objekte mit Referenz-Semantik, ein Feld vom Typ Node ist automatisch eine Referenz. In Rust ist das anders — ein Feld liegt direkt im Struct, mit voller Größe. Und genau das funktioniert bei Rekursion nicht.

Schreiben wir die naive Variante als Test, und schauen wir uns die Reaktion des Compilers an:

Rust Naive Variante — kompiliert nicht
enum List {
    Node(i32, List),        // List enthält wieder List — direkt
    End,
}

Der Compiler antwortet:

text rustc-Ausgabe
error[E0072]: recursive type `List` has infinite size
 --> src/main.rs:1:1
  |
1 | enum List {
  | ^^^^^^^^^
2 |     Node(i32, List),
  |               ---- recursive without indirection
  |
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`)
      to break the cycle
  |
2 |     Node(i32, Box<List>),
  |               ++++    +

Diese Fehlermeldung ist Gold wert, weil sie zwei Dinge gleichzeitig sagt: was schiefläuft („recursive without indirection") und wie du es behebst („insert some indirection, e.g., a Box"). Das gehört zu den Fehler-Klassen, bei denen rustc dir die Lösung direkt mitliefert.

Was ist „indirection" konkret? Wenn der Compiler die Größe von List berechnen will, geht er rekursiv vor: List enthält ein Node, das enthält ein i32 (4 Bytes) plus ein List. Die Größe von List ist also 4 + Größe(List) — eine Gleichung ohne endliche Lösung. Mit Box<List> ändert sich das: List enthält 4 + 8 (Box-Pointer). Bekannte, fixe Größe.

Rust Erlaubte Form mit Box
enum List {
    Node(i32, Box<List>),
    End,
}

fn main() {
    // 1 -> 2 -> 3 -> End
    let list = List::Node(
        1,
        Box::new(List::Node(
            2,
            Box::new(List::Node(
                3,
                Box::new(List::End),
            )),
        )),
    );

    // Traversal
    let mut cursor = &list;
    while let List::Node(value, rest) = cursor {
        println!("{value}");
        cursor = rest;
    }
}

Jetzt kompiliert es. Die Box ist hier nicht eine Optimierung, sondern eine notwendige Indirektion: ohne sie ist der Typ buchstäblich nicht repräsentierbar. Das gilt für jede rekursive Struktur — irgendwo in der Schleife muss eine Indirektion stehen, sonst hat der Compiler nichts zum Aufhören.

Beachte beim Traversal die Verwendung von cursor = rest: rest ist &Box<List> (eine Referenz in den Node hinein), und durch Deref-Coercion wird daraus automatisch ein &List. Die Box ist beim Lesen also unsichtbar — sie wirkt wie eine direkte Referenz.

Binärbaum

Rust Binärbaum
struct Node {
    value: i32,
    left: Option<Box<Node>>,
    right: Option<Box<Node>>,
}

impl Node {
    fn new(value: i32) -> Self {
        Node { value, left: None, right: None }
    }

    fn insert(&mut self, new_value: i32) {
        if new_value < self.value {
            match &mut self.left {
                Some(left_subtree) => left_subtree.insert(new_value),
                None => self.left = Some(Box::new(Node::new(new_value))),
            }
        } else {
            match &mut self.right {
                Some(right_subtree) => right_subtree.insert(new_value),
                None => self.right = Some(Box::new(Node::new(new_value))),
            }
        }
    }
}

Lass uns die Struktur dieses Beispiels durchgehen, weil sie ein Muster zeigt, das in vielen rekursiven Datenstrukturen wiederkommt.

Option<Box<Node>> ist die idiomatische Schreibweise für „optionaler Sub-Baum". Drei Zustände sind dort kodiert:

  • None — kein Kind existiert.
  • Some(Box::new(Node::new(...))) — ein neues Kind wird auf dem Heap angelegt.
  • Some(box) — ein bereits existierendes Kind, in dem rekursiv weitergesucht wird.

Innerhalb von insert kannst du beim match &mut self.left schön sehen, wie sich die zwei Varianten unterschiedlich verhalten: wenn schon ein Kind da ist, ruft der Code rekursiv left_subtree.insert(new_value) darauf auf — das landet wieder in derselben Methode, eine Ebene tiefer im Baum. Wenn noch keins da ist, hängt der Code ein neues Box::new(Node::new(new_value)) ein und die Rekursion stoppt.

Eine besonders elegante Eigenschaft, die hier mitläuft: Option<Box<T>> ist genauso groß wie ein nackter Box-Pointer — typischerweise 8 Bytes. Der Compiler erkennt, dass ein gültiger Box-Pointer niemals null sein kann, und kodiert das None als „all-zeros". Das nennt sich Null-Pointer-Optimierung (NPO). Ohne sie bräuchte Option<Box<T>> ein zusätzliches Diskriminator-Byte und wäre dadurch 16 Bytes groß (Alignment). Mit ihr sparst du die Hälfte, ohne dass du als Programmierer irgendetwas dafür tun musst.

Daraus folgt: ein leerer Baum (None) und ein nicht-leerer Baum (Some(box)) belegen im Speicher exakt gleich viel Platz. Das ist die Art von Detail, die Rust-Code so effizient macht, ohne dass es im Source-Code sichtbar wäre.

Anwendung 2: Trait-Objekte

Die zweite Hauptanwendung sind Trait-Objekte — Werte, deren konkreter Typ erst zur Laufzeit feststeht. Das ausführliche Thema ist im Traits-Kapitel; hier konzentrieren wir uns darauf, warum sie eine Box brauchen.

Der Kern des Problems heißt Sizing. Eine normale Funktion oder ein normales Feld muss vom Compiler eine genaue Größe in Bytes bekommen — sonst kann er nicht den Stack-Frame oder das Struct-Layout berechnen. dyn Animal (die Schreibweise „irgendetwas, das Animal implementiert") verletzt diese Anforderung: der konkrete Typ könnte Dog (z.B. 0 Bytes als Unit-Struct) sein, oder Cat (auch 0 Bytes), oder eine andere Struktur mit 100 Bytes State. Verschiedene Implementierer haben verschiedene Größen — daher ist dyn Animal ein unsized (DST, Dynamically Sized Type).

Unsized-Typen kannst du nicht direkt als Wert in einer Variable speichern oder als Funktions-Argument übergeben. Aber hinter einem Pointer geht es immer — ein Pointer hat eine bekannte Größe (8 Bytes), egal wohin er zeigt. Genau das macht Box<dyn Trait>: die Box ist 8 Bytes (eigentlich sogar 16, dazu gleich), der konkrete Wert lebt auf dem Heap, und die Welt ist wieder in Ordnung.

Schauen wir uns das ganze Pattern an:

Rust Box dyn Trait
trait Animal {
    fn sound(&self) -> String;
}

struct Dog;
impl Animal for Dog {
    fn sound(&self) -> String { String::from("Wuff") }
}

struct Cat;
impl Animal for Cat {
    fn sound(&self) -> String { String::from("Miau") }
}

fn main() {
    let animals: Vec<Box<dyn Animal>> = vec![
        Box::new(Dog),
        Box::new(Cat),
        Box::new(Dog),
    ];

    for animal in &animals {
        println!("{}", animal.sound());
    }
}

Die Box ist hier doppelt nützlich. Sie erlaubt die heterogene Sammlung — verschiedene konkrete Typen (Dog, Cat) leben friedlich nebeneinander in demselben Vec. Und sie löst das Sizing-Problem — die konkreten Typen sind unterschiedlich groß, die Pointer aber gleich.

Eine technische Notiz: ein Box<dyn Animal> ist tatsächlich 16 Bytes groß, nicht 8 — er ist ein sogenannter fat pointer. Acht Bytes davon sind der eigentliche Daten-Pointer (zeigt auf den Dog/Cat-Wert), die anderen acht sind ein Pointer auf eine Vtable: eine Tabelle mit Funktions-Pointern, die zur Laufzeit den richtigen sound()-Aufruf findet. Wenn du animal.sound() schreibst, schlägt der Compiler in der Vtable nach, welche konkrete sound-Implementation aufzurufen ist. Dieser indirekte Aufruf ist der Preis für Dynamic Dispatch — ein paar CPU-Zyklen mehr als bei einem statisch aufgelösten Aufruf, dafür Flexibilität. Mehr im Traits-Kapitel und im dyn-Artikel speziell.

Trait-Objekte hinter Box sind so verbreitet, dass viele Stdlib-APIs sie als Default-Form erwarten. Das prominenteste Beispiel ist Box<dyn Error> als Fehler-Typ in CLI-Programmen — fn main() -> Result<(), Box<dyn Error>> ist die idiomatische Signatur für Programme, die unterschiedliche Fehler-Typen sauber via ? propagieren wollen.

Anwendung 3: Funktions-Rückgabe mit unbekanntem Typ

Eine weitere klassische Anwendung: eine Funktion, die je nach Eingabe verschiedene konkrete Typen zurückgibt — alle mit derselben Trait-Implementierung.

Rust Factory
# trait Animal { fn sound(&self) -> String; }
# struct Dog;
# impl Animal for Dog { fn sound(&self) -> String { String::from("Wuff") } }
# struct Cat;
# impl Animal for Cat { fn sound(&self) -> String { String::from("Miau") } }

fn create(kind: &str) -> Box<dyn Animal> {
    match kind {
        "dog" => Box::new(Dog),
        "cat" => Box::new(Cat),
        _ => Box::new(Dog),
    }
}

fn main() {
    let animal = create("cat");
    println!("{}", animal.sound());
}

Dieses Beispiel zeigt einen oft übersehenen Unterschied. Eine ähnliche Funktion mit -> impl Animal würde nicht kompilieren:

Rust impl Trait — geht nicht für Multi-Typ
# trait Animal { fn sound(&self) -> String; }
# struct Dog;
# impl Animal for Dog { fn sound(&self) -> String { String::from("Wuff") } }
# struct Cat;
# impl Animal for Cat { fn sound(&self) -> String { String::from("Miau") } }

// FEHLER:
// fn create(kind: &str) -> impl Animal {
//     match kind {
//         "dog" => Dog,            // Typ Dog
//         "cat" => Cat,            // Typ Cat
//         _ => Dog,
//     }
// }
// error[E0308]: `match` arms have incompatible types

Der Grund: impl Trait als Rückgabe-Typ heißt nicht „beliebiger Typ, der Animal implementiert", sondern „genau ein konkreter Typ, den der Compiler aus dem Funktions-Body ableitet, und der nach außen als Animal versteckt wird". Über alle match-Arme hinweg muss derselbe konkrete Typ rauskommen. Das passt für Iteratoren oder Closures (wo der Body genau einen Typ erzeugt), aber nicht für Factory-Funktionen mit mehreren möglichen Ausgaben.

Box<dyn Animal> ist die richtige Lösung für Factory-Pattern: jeder Arm darf einen anderen konkreten Typ in die Box packen, weil zur Laufzeit über die Vtable dispatched wird.

Box als Fehler-Container — Box dyn Error

Eine sehr verbreitete Anwendung in CLI-Programmen: Box<dyn Error> als Catch-All-Error-Typ.

Rust Box dyn Error
use std::error::Error;
use std::fs;

fn load_number(path: &str) -> Result<i32, Box<dyn Error>> {
    let content = fs::read_to_string(path)?;      // io::Error → Box<dyn Error>
    let n: i32 = content.trim().parse()?;         // ParseIntError → Box<dyn Error>
    Ok(n)
}

fn main() -> Result<(), Box<dyn Error>> {
    let n = load_number("number.txt")?;
    println!("Loaded: {n}");
    Ok(())
}

Box<dyn Error> akzeptiert jeden Typ, der Error implementiert. Der ?-Operator konvertiert automatisch via From-Impl (jeder Error-Typ hat From<E> for Box<dyn Error>). Das ist die ergonomische Form für Anwendungs-Code, wo programmatische Differenzierung nicht nötig ist — du willst nur den Fehler propagieren und am Ende ausgeben.

Für Library-Code ist Box<dyn Error> zu unspezifisch — dort nutzt man eigene Error-Enums oder thiserror. Siehe Error-Handling-Kapitel.

Box::leak — Speicher gewollt nicht freigeben

Eine besondere Methode: Box::leak. Sie nimmt die Box und gibt eine &'static-Referenz auf den inneren Wert zurück — der Speicher wird bewusst nicht freigegeben.

Rust Box::leak
fn lade_config() -> &'static str {
    // Aus Env, Datei o.ä. — hier vereinfacht:
    let dynamisch = String::from("api.example.com");
    // Box::leak: heap-allozierter String wird zu &'static str
    Box::leak(dynamisch.into_boxed_str())
}

fn main() {
    let host: &'static str = lade_config();
    println!("Host: {host}");

    // Der String wird NIE freigegeben — bis Programmende.
}

Wozu? Manche APIs verlangen &'static-Referenzen (klassisch: thread::spawn-Closures, Async-Tasks, langlebige globale Konstanten). Wenn du den String erst zur Laufzeit erzeugen kannst, ist Box::leak die saubere Lösung: einmal beim Programmstart leaken, danach hast du eine permanente Referenz.

Wichtig: das ist eine bewusste Memory-Leak-Operation. Für regelmäßig erzeugte Daten ist sie NICHT geeignet — sonst wächst der Speicher unbegrenzt. Klassische Anwendungs-Fälle sind einmalige Init beim Start (Config-Parameter, geladene Templates, Lookup-Tabellen).

Alternative für solche Patterns: OnceLock (lazy-init 'static, ohne Leak — siehe Lifetimes-Kapitel).

Box und das Drop-Verhalten

Eine Box macht beim Drop genau eins: sie ruft drop auf den inneren Wert auf und gibt den Heap-Speicher frei.

Rust Drop-Verhalten
struct Loud(String);
impl Drop for Loud {
    fn drop(&mut self) {
        println!("Drop: {}", self.0);
    }
}

fn main() {
    let b = Box::new(Loud(String::from("a")));
    println!("vor Drop");
}   // hier wird b gedroppt → Loud::drop läuft → Heap-Allocation freigegeben

Ausgabe:

text
vor Drop
Drop: a

Das ist deterministisch: kein Garbage Collector, kein zufälliger Zeitpunkt. Sobald die Box ihren Scope verlässt, läuft Drop, der Heap-Speicher ist sofort frei.

Performance-Charakteristik

Eine Box-Allokation kostet einen Heap-Allocator-Call. Auf modernen Allocators sind das wenige Nanosekunden, aber in einem Hot-Loop summiert sich das.

Faustregeln:

  • Für einmalige Allocations (Programm-Init, Plugin-Loading, Trait-Objekt-Registry) ist der Cost irrelevant.
  • Für häufige Allocations (z.B. ein Box::new in jedem Loop-Iteration) lohnt sich, einen Pool oder eine Vec-basierte Variante zu nutzen.
  • Bei rekursiven Strukturen mit vielen kleinen Knoten kann ein Arena-Allocator (Stichwort: bumpalo-Crate) deutlich schneller sein.

Der Deref-Aufruf durch die Box ist im optimierten Build typischerweise kostenlos — der Compiler inlined die Deref-Operation. Bei Box<dyn Trait> ist der Method-Call ein indirekter Aufruf via Vtable (mehr im Traits-Kapitel), aber das ist kein Box-spezifischer Cost, sondern ein Dyn-Trait-Cost.

Praxis: Box im echten Code

AST für einen Mini-Parser

Rust AST
#[derive(Debug)]
enum Expr {
    Number(f64),
    Add(Box<Expr>, Box<Expr>),
    Sub(Box<Expr>, Box<Expr>),
    Mul(Box<Expr>, Box<Expr>),
    Paren(Box<Expr>),
}

impl Expr {
    fn evaluate(&self) -> f64 {
        match self {
            Expr::Number(n) => *n,
            Expr::Add(a, b) => a.evaluate() + b.evaluate(),
            Expr::Sub(a, b) => a.evaluate() - b.evaluate(),
            Expr::Mul(a, b) => a.evaluate() * b.evaluate(),
            Expr::Paren(e) => e.evaluate(),
        }
    }
}

fn main() {
    // (1 + 2) * 3
    let expr = Expr::Mul(
        Box::new(Expr::Paren(Box::new(Expr::Add(
            Box::new(Expr::Number(1.0)),
            Box::new(Expr::Number(2.0)),
        )))),
        Box::new(Expr::Number(3.0)),
    );
    assert_eq!(expr.evaluate(), 9.0);
}

Klassisches Pattern in Interpretern und Compilern: ein rekursiver Enum mit Box an jeder Rekursions-Stelle. Die evaluate-Methode ist eine direkte Übersetzung der Ausdrucks-Semantik in Rust-Code.

Plugin-Registry

Rust Plugin-Registry
trait Plugin {
    fn name(&self) -> String;
    fn process(&self, input: &str) -> String;
}

struct UppercasePlugin;
impl Plugin for UppercasePlugin {
    fn name(&self) -> String { String::from("uppercase") }
    fn process(&self, input: &str) -> String { input.to_uppercase() }
}

struct ReversePlugin;
impl Plugin for ReversePlugin {
    fn name(&self) -> String { String::from("reverse") }
    fn process(&self, input: &str) -> String {
        input.chars().rev().collect()
    }
}

struct Registry {
    plugins: Vec<Box<dyn Plugin>>,
}

impl Registry {
    fn new() -> Self { Self { plugins: Vec::new() } }
    fn register(&mut self, p: Box<dyn Plugin>) {
        self.plugins.push(p);
    }
    fn apply_all(&self, input: &str) {
        for p in &self.plugins {
            println!("{}: {}", p.name(), p.process(input));
        }
    }
}

fn main() {
    let mut reg = Registry::new();
    reg.register(Box::new(UppercasePlugin));
    reg.register(Box::new(ReversePlugin));
    reg.apply_all("Hello");
}

Heterogene Sammlung verschiedener Plugin-Typen. Vec<Box<dyn Plugin>> ist hier die einzige Form, die das überhaupt zulässt — mit Vec<P: Plugin> müssten alle Elemente vom selben konkreten Typ sein.

Große Struct vermeiden den Stack zu füllen

Rust Große Struct auf Heap
struct LargeBuffer {
    data: [u8; 1024 * 1024],     // 1 MB
}

fn naive() -> LargeBuffer {
    LargeBuffer { data: [0; 1024 * 1024] }   // landet auf dem Stack — zu groß
}

fn better() -> Box<LargeBuffer> {
    Box::new(LargeBuffer { data: [0; 1024 * 1024] })   // sofort auf den Heap
}

fn main() {
    // Stack-Größe in vielen Threads: 2 MB. Ein 1-MB-Buffer auf dem Stack
    // ist riskant — bei mehrfacher Rekursion oder kleinen Thread-Stacks
    // gibt es einen Stack-Overflow.
    let _b = better();
}

Bei großen Werten ist die Stack-Variante riskant. Box::new(...) verlagert die Allokation auf den Heap, der Stack hält nur den 8-Byte-Pointer. Faustregel: alles über ein paar Kilobyte sollte in einer Box landen.

Trait-Methode mit Trait-Objekt-Argument

Rust Pipeline
trait Filter {
    fn apply(&self, input: &str) -> String;
}

struct Trim;
impl Filter for Trim {
    fn apply(&self, s: &str) -> String { s.trim().to_string() }
}

struct Lower;
impl Filter for Lower {
    fn apply(&self, s: &str) -> String { s.to_lowercase() }
}

fn run_pipeline(filters: Vec<Box<dyn Filter>>, input: &str) -> String {
    let mut current = input.to_string();
    for f in &filters {
        current = f.apply(&current);
    }
    current
}

fn main() {
    let result = run_pipeline(
        vec![Box::new(Trim), Box::new(Lower)],
        "  HALLO  ",
    );
    assert_eq!(result, "hallo");
}

Heterogene Filter-Pipeline. Verschiedene Filter-Typen werden als Box<dyn Filter> zu einer Liste kombiniert und sequenziell angewendet.

Box mit Config-Loader

Rust Config aus Env
fn load_api_url() -> &'static str {
    let url = std::env::var("API_URL")
        .unwrap_or_else(|_| String::from("http://localhost:3000"));
    Box::leak(url.into_boxed_str())
}

fn main() {
    let api_url: &'static str = load_api_url();
    println!("API: {api_url}");

    // Lebt für die gesamte Programm-Laufzeit
    std::thread::spawn(move || {
        println!("Thread sieht: {api_url}");
    }).join().unwrap();
}

Pragmatischer Config-Loader: einmal beim Start die URL aus der Env lesen, per Box::leak zu &'static str machen, in allen Threads nutzen. Memory-Leak ist hier akzeptabel (einmaliger Init, fester Wert bis Programmende).

Tagged-Union mit großen Varianten

Rust Box in Enum-Variante
enum Message {
    Small(u32),
    Medium { name: String, age: u32 },
    Huge(Box<[u8; 1024 * 1024]>),    // Box, damit Enum nicht riesig wird
}

fn main() {
    let _s = Message::Small(42);
    let _m = Message::Medium {
        name: String::from("Alice"),
        age: 30,
    };
    let _h = Message::Huge(Box::new([0; 1024 * 1024]));
    // Alle drei Varianten haben kompakte Stack-Größe — die größte ist
    // die Medium-Variante mit String (24 Bytes) + u32, nicht die
    // Huge-Variante mit ihrem 1-MB-Inhalt.
}

Wichtiges Pattern: ein Enum hat die Größe seiner größten Variante. Wenn eine Variante riesig ist (z.B. ein 1-MB-Array), wird das gesamte Enum so groß. Box an der riesigen Variante reduziert das auf einen Pointer — die Stack-Größe bleibt klein, der Heap-Speicher nur dann allokiert, wenn die Variante tatsächlich gebraucht wird.

Interessantes

Box = ein Owner, Heap-Allokation, Drop räumt auf.

Der einfachste Smart Pointer. Genau ein Owner, Wert auf dem Heap, beim Verlassen des Scopes wird der Heap-Speicher automatisch freigegeben.

Size_of(Box) = 8 Bytes auf 64-Bit, unabhängig von T.

Egal ob Box<i32> oder Box<[u8; 1MB]> — die Box selbst ist ein Pointer. Das löst das Sizing-Problem bei rekursiven Strukturen und Trait-Objekten.

Hauptanwendung 1: rekursive Strukturen.

Verkettete Listen, Bäume, ASTs. Ohne Box würde der Compiler eine unendliche Typ-Größe sehen und ablehnen. Box bringt die nötige Indirektion.

Hauptanwendung 2: Box für Trait-Objekte.

dyn Trait ist unsized — geht nur hinter einem Pointer. Box<dyn Trait> ist owned, heap-alloziert, runtime-polymorph. Klassisch für heterogene Sammlungen und Plugin-Architekturen.

Option> ist nicht größer als ein nackter Pointer.

Null-Pointer-Optimierung: None wird als Null kodiert, Some(box) als der echte Pointer. Damit kostet die Option-Wrapper nichts extra.

Box::leak erzeugt &'static — bewusst nicht freigegeben.

Heap-Speicher lebt bis Programmende. Nur für einmalige Init beim Start akzeptabel, niemals in Hot-Loops. Alternative: OnceLock für lazy-init 'static.

Box für CLI-Programme.

Akzeptiert jeden Error-Typ via From-Impl. Der ?-Operator konvertiert automatisch. Ergonomische Form für Anwendungs-Code, in Library-Code dagegen zu unspezifisch.

Performance: Heap-Allocation kostet — in Hot-Loops bedenken.

Eine Box-Allokation kostet einen Allocator-Call. In normalen APIs irrelevant, in Hot-Loops summiert es sich. Für viele kleine Allocations gibt es Arena-Allocators (bumpalo-Crate).

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Smart Pointers

Zur Übersicht