Const-Generics sind eine besondere Form von Typ-Parametern: statt eines Typs ist der Parameter ein konstanter Wert. Du kennst sie bereits aus den Stdlib-Arrays — [T; N] ist generisch über den Element-Typ T und die Länge N (eine usize-Konstante). Mit Const-Generics kannst du eigene Typen schreiben, die feste Größen typsicher mitführen — Mathematische Vektoren Vec3<f64>, fixe Buffer Buffer<1024>, Matrizen Matrix<f64, 3, 4>. Dieser Artikel zeigt die Syntax, die typischen Anwendungsfälle und die aktuellen Einschränkungen.

Was Const-Generics sind

Bei normalen Generics ist der Parameter ein Typ:

Rust Type-Generic
struct Container<T> {
    wert: T,
}

Bei Const-Generics ist der Parameter ein konstanter Wert:

Rust Const-Generic
struct Buffer<const N: usize> {
    daten: [u8; N],
}

fn main() {
    let b1: Buffer<32> = Buffer { daten: [0; 32] };
    let b2: Buffer<1024> = Buffer { daten: [0; 1024] };
    // b1 und b2 sind VERSCHIEDENE Typen
    let _ = (b1.daten, b2.daten);
}

Die Syntax <const N: usize> führt einen Const-Parameter N vom Typ usize ein. Der Parameter ist im Body als Konstante verfügbar — [u8; N] referenziert ihn als Array-Länge.

Wichtig: Buffer<32> und Buffer<1024> sind verschiedene Typen. Auf Typ-Ebene sind sie unterschiedlich, der Compiler verhindert versehentliche Vermischung. Eine Funktion, die Buffer<32> erwartet, akzeptiert keinen Buffer<1024>.

Erlaubt sind als Const-Parameter aktuell: alle Integer-Typen (u8, i32, usize, etc.), bool, char. Andere Typen (z. B. String, eigene Structs) werden noch nicht unterstützt.

Das prominenteste Beispiel: Arrays

Du arbeitest schon längst mit Const-Generics — [T; N]-Arrays sind genau das.

Rust Array als Const-Generic
fn main() {
    let a3: [i32; 3] = [1, 2, 3];
    let a5: [i32; 5] = [1, 2, 3, 4, 5];

    // [i32; 3] und [i32; 5] sind verschiedene Typen
    // Eine Funktion, die [i32; 3] erwartet, akzeptiert kein [i32; 5]

    println!("{}", a3.len());      // 3, zur Compile-Zeit bekannt
    println!("{}", a5.len());      // 5
}

[i32; 3] ist syntaktischer Zucker für etwas, das du auch mit eigenem Code modellieren kannst. Die Länge ist Teil des Typs und vom Compiler überprüft. Anders als bei Vec<i32> (Heap-Wachstum, Länge zur Laufzeit) ist die Array-Größe statisch fixiert.

Die Stdlib hat viele Methoden, die Const-Generics nutzen. iter().chunks::<3>(), array::from_fn::<i32, 5, _>(|i| i as i32), und so weiter — die spitze Klammer mit einer Zahl ist immer ein Hinweis auf einen Const-Generic.

Eigene Strukturen mit Const-Generics

Rust Eigener Vec3
struct Vector<const N: usize> {
    data: [f64; N],
}

impl<const N: usize> Vector<N> {
    fn neu(data: [f64; N]) -> Self {
        Vector { data }
    }

    fn skalar_produkt(&self, other: &Vector<N>) -> f64 {
        let mut summe = 0.0;
        for i in 0..N {
            summe += self.data[i] * other.data[i];
        }
        summe
    }
}

fn main() {
    let v3 = Vector::<3>::neu([1.0, 2.0, 3.0]);
    let w3 = Vector::<3>::neu([4.0, 5.0, 6.0]);
    assert_eq!(v3.skalar_produkt(&w3), 32.0);
    // v3.skalar_produkt(&Vector::<5>::neu(...)) — Compile-Fehler:
    // verschiedene Dimensionen sind verschiedene Typen
}

Ein generischer Vektor mit fester Dimension. Vector<3> ist ein 3D-Vektor, Vector<5> ein 5D-Vektor — beide nutzen denselben Code, aber sind unterschiedliche Typen.

Der Compiler erzwingt Dimensions-Konsistenz: skalar_produkt verlangt einen Vector der gleichen Länge N. Zwei verschiedene Dimensionen können nicht versehentlich gemischt werden. Bei Vec<f64>-basierten Implementierungen wäre das nur zur Laufzeit prüfbar.

Matrizen mit zwei Const-Parametern

Rust Matrix
struct Matrix<const ROWS: usize, const COLS: usize> {
    data: [[f64; COLS]; ROWS],
}

impl<const R: usize, const C: usize> Matrix<R, C> {
    fn neu() -> Self {
        Matrix { data: [[0.0; C]; R] }
    }

    fn set(&mut self, r: usize, c: usize, v: f64) {
        self.data[r][c] = v;
    }
}

fn main() {
    let m3x4: Matrix<3, 4> = Matrix::neu();
    let m5x5: Matrix<5, 5> = Matrix::neu();
    // m3x4 und m5x5 sind verschiedene Typen
    let _ = (m3x4, m5x5);
}

Zwei Const-Parameter modellieren Matrizen-Dimensionen. Matrix<3, 4> ist eine 3x4-Matrix, Matrix<5, 5> eine 5x5-Matrix. Die Dimensionen sind im Typ-System fest verankert — falsche Matrix-Multiplikationen werden zur Compile-Zeit gefangen.

Const-Parameter in Funktionen

Const-Generics funktionieren nicht nur in Structs, sondern auch in Funktionen.

Rust Generic-Funktion
fn wiederholen<const N: usize>(wert: i32) -> [i32; N] {
    [wert; N]
}

fn main() {
    let a: [i32; 5] = wiederholen(42);     // Type-Inference
    let b = wiederholen::<3>(7);            // Turbofish
    assert_eq!(a, [42; 5]);
    assert_eq!(b, [7; 3]);
}

wiederholen::<3>(7) ruft die Funktion mit N = 3 auf. Der Compiler generiert eine spezialisierte Version für jeden Const-Wert (Monomorphisierung wie bei Typ-Generics).

Die Type-Inference funktioniert auch hier: wenn der Rückgabe-Typ explizit ist (let a: [i32; 5] = ...), kann der Compiler N = 5 daraus ableiten. Sonst brauchst du den Turbofish.

Operationen auf Const-Parametern

Aktuell (Stand Rust 1.78+) sind die erlaubten Operationen auf Const-Parametern noch eingeschränkt.

Rust Was geht
fn ersten<const N: usize>(arr: [i32; N]) -> i32 {
    arr[0]
}

// Konstante Berechnungen sind eingeschränkt:
// fn verdoppelt_groesse<const N: usize>() -> [i32; N * 2] {
//     [0; N * 2]
// }
// — funktioniert in stable Rust noch NICHT für arbitrary Berechnungen.

Was stable funktioniert:

  • Const-Parameter als Größe in Array-Typen.
  • Vergleich mit Konstanten in where-Klauseln (eingeschränkt).
  • Der Const-Wert als normale Variable im Funktions-Body.

Was noch nicht stable ist (nur Nightly):

  • Komplexe arithmetische Ausdrücke auf Const-Parametern in Typen ([T; N * 2]).
  • Const-Generics über non-primitive Typen.
  • Const-Bounds (etwa „N muss gerade sein").

Diese Einschränkungen entspannen sich mit jeder Rust-Version. Das Feature ist in aktiver Entwicklung — ein Stable-Release des sogenannten generic_const_exprs wird die meisten Beschränkungen aufheben.

Wann Const-Generics sinnvoll sind

Const-Generics sind nicht für jeden Anwendungsfall. Sie machen Sinn, wenn:

Die Größe ist semantisch wichtig und sollte vom Typ-System überprüft werden. Bei mathematischen Vektoren, Matrizen, Quaternionen ist die Dimension Teil der Identität — Vec3Vec4.

Die Größe ist zur Compile-Zeit bekannt. Const-Generics brauchen einen konstanten Wert. Wenn die Größe erst zur Laufzeit feststeht, brauchst du Vec<T> oder einen anderen Heap-Container.

Du willst Stack-Allokation. [T; N] lebt auf dem Stack (wie alle Arrays). Das ist effizient für kleine, feste Größen. Bei großen Arrays musst du Box<[T; N]> nutzen, um Stack-Overflow zu vermeiden.

Performance-kritischer Code mit fester Iteration. Wenn der Compiler die Loop-Länge kennt, kann er sie aggressiv optimieren — Unrolling, Vectorisierung, SIMD-Auto-Generation.

Const-Generics sind nicht sinnvoll, wenn:

  • Die Größe dynamisch ist (immer Vec<T>).
  • Du nur kosmetische Typ-Disambiguierung willst (lieber Newtype).
  • Die Compile-Zeit-Strenge die Bequemlichkeit nicht wert ist (manche APIs werden komplexer).

Praxis: Const-Generics im echten Code

Geometrische Vektoren

Rust Vec3 / Vec4
#[derive(Clone, Copy, Debug)]
pub struct Vector<const N: usize> {
    data: [f64; N],
}

impl<const N: usize> Vector<N> {
    pub fn neu(data: [f64; N]) -> Self {
        Vector { data }
    }

    pub fn laenge(&self) -> f64 {
        self.data.iter().map(|x| x * x).sum::<f64>().sqrt()
    }

    pub fn skalar_produkt(&self, other: &Vector<N>) -> f64 {
        self.data.iter().zip(other.data.iter())
            .map(|(a, b)| a * b)
            .sum()
    }
}

// Type-Aliase für gängige Dimensionen
pub type Vec2 = Vector<2>;
pub type Vec3 = Vector<3>;
pub type Vec4 = Vector<4>;

Klassische Anwendung in Game-Engines, Physics-Sim, Grafik-Bibliotheken. Eine generische Implementation, drei Type-Aliase für die häufigen Dimensionen. Mathematische Operationen sind dimension-sicher.

Fixe Buffer

Rust Stack-Buffer
pub struct Buffer<const SIZE: usize> {
    data: [u8; SIZE],
    used: usize,
}

impl<const SIZE: usize> Buffer<SIZE> {
    pub fn neu() -> Self {
        Buffer { data: [0; SIZE], used: 0 }
    }

    pub fn push(&mut self, byte: u8) -> Result<(), &'static str> {
        if self.used >= SIZE {
            return Err("Buffer voll");
        }
        self.data[self.used] = byte;
        self.used += 1;
        Ok(())
    }
}

fn main() {
    let mut b: Buffer<256> = Buffer::neu();
    for _ in 0..256 { b.push(0).unwrap(); }
    assert!(b.push(0).is_err());      // 257. Byte → Error
}

Stack-allozierter Buffer mit fester Größe. Bei kleinen Größen (≤ 4 KB) eine sehr effiziente Alternative zu Heap-basierten Buffern — keine Allokation, keine Deallokation, perfekte Cache-Lokalität.

Kryptografische Hashes

Rust Hash-Container
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Hash<const LEN: usize>([u8; LEN]);

impl<const LEN: usize> Hash<LEN> {
    pub fn neu(bytes: [u8; LEN]) -> Self {
        Hash(bytes)
    }

    pub fn bytes(&self) -> &[u8] {
        &self.0
    }
}

pub type Sha256Hash = Hash<32>;
pub type Sha512Hash = Hash<64>;
pub type Md5Hash = Hash<16>;

Verschiedene Hash-Algorithmen produzieren verschieden lange Hashes. Mit Const-Generics werden sie typsicher unterschieden — du kannst keinen MD5 mit einem SHA-256 vertauschen.

Matrix-Operationen mit Dimensionssicherheit

Rust Matrix-Multiplikation
pub struct Matrix<const R: usize, const C: usize> {
    data: [[f64; C]; R],
}

impl<const R: usize, const C: usize> Matrix<R, C> {
    pub fn neu() -> Self {
        Matrix { data: [[0.0; C]; R] }
    }
}

// Matrix-Multiplikation: (R1×C1) × (R2×C2) → (R1×C2)
// Bedingung: C1 == R2 — vom Typ-System geprüft!
// (Vereinfachte Demo — echte Implementierung bräuchte mehr Bounds)
fn multipliziere<const R: usize, const M: usize, const C: usize>(
    _a: &Matrix<R, M>,
    _b: &Matrix<M, C>,
) -> Matrix<R, C> {
    Matrix::neu()
}

fn main() {
    let a: Matrix<3, 4> = Matrix::neu();
    let b: Matrix<4, 2> = Matrix::neu();
    let c: Matrix<3, 2> = multipliziere(&a, &b);
    let _ = c;

    // let invalid = multipliziere(&a, &Matrix::<5, 2>::neu());
    // Compile-Fehler: 4 != 5
}

Lineare Algebra wird typsicher. Die Multiplikations-Funktion verlangt, dass die zweite Dimension der ersten Matrix der ersten Dimension der zweiten gleicht. Falsche Multiplikationen werden zur Compile-Zeit gefangen.

Bit-Vektoren

Rust BitVec
pub struct BitVec<const N: usize> {
    bits: [u8; N],
}

impl<const N: usize> BitVec<N> {
    pub fn neu() -> Self {
        BitVec { bits: [0; N] }
    }

    pub fn setze(&mut self, index: usize) {
        if index < N * 8 {
            self.bits[index / 8] |= 1 << (index % 8);
        }
    }

    pub fn ist_gesetzt(&self, index: usize) -> bool {
        if index < N * 8 {
            (self.bits[index / 8] & (1 << (index % 8))) != 0
        } else {
            false
        }
    }
}

Bit-Vektor mit fester Byte-Größe. Effizient für Bit-Flag-Sets mit bekannter Maximalgröße.

Test-Daten mit fester Länge

Rust Test-Helper
fn test_array<const N: usize>(default: i32) -> [i32; N] {
    [default; N]
}

fn main() {
    let a = test_array::<10>(0);
    let b = test_array::<100>(-1);
    assert_eq!(a.len(), 10);
    assert_eq!(b[0], -1);
}

Helper-Funktionen, die Arrays mit konfigurierbarer Größe erzeugen. In Tests sehr nützlich, um Test-Daten in der gewünschten Form anzulegen.

Konstante Lookup-Tabellen

Rust Lookup-Tabelle
pub struct LookupTable<const N: usize> {
    werte: [f64; N],
}

impl<const N: usize> LookupTable<N> {
    pub fn new(f: impl Fn(usize) -> f64) -> Self {
        let mut werte = [0.0; N];
        for i in 0..N {
            werte[i] = f(i);
        }
        LookupTable { werte }
    }

    pub fn get(&self, i: usize) -> Option<f64> {
        self.werte.get(i).copied()
    }
}

fn main() {
    // Vorausberechnete Sinustabelle für 360 Grad
    let sin_tabelle: LookupTable<360> = LookupTable::new(|i| (i as f64).to_radians().sin());
    assert!((sin_tabelle.get(90).unwrap() - 1.0).abs() < 1e-9);
}

Vorausberechnete Lookup-Tabellen mit fester Größe. In Performance-kritischem Code (Grafik, Audio-DSP, Spiele) sehr typisch — die Tabellen-Größe ist Teil des Typs, die Berechnung passiert einmalig beim Erzeugen.

Interessantes

Const-Generics = Typ-Parameter mit konstantem Wert.

Statt eines Typs wie T ist der Parameter ein Wert wie N: usize. Erlaubt sind aktuell alle Integer-Typen, bool, char — Strings und eigene Typen werden noch nicht unterstützt.

[T; N] ist Const-Generic.

Arrays sind das prominenteste Stdlib-Beispiel. [i32; 3] und [i32; 5] sind verschiedene Typen — Längen-Konsistenz wird zur Compile-Zeit erzwungen.

Verschiedene Werte = verschiedene Typen.

Buffer<32> und Buffer<1024> sind aus Sicht des Typ-Systems völlig unterschiedlich. Vermischungen sind Compile-Fehler. Das ist genau die Stärke des Features.

Compile-Zeit-Spezialisierung wie bei Type-Generics.

Für jeden konkreten Const-Wert wird eine eigene spezialisierte Version generiert (Monomorphisierung). Konsequenz: gleiche Trade-offs bei Binary-Größe und Compile-Zeit.

Stack-Allokation für feste Größen.

[T; N] und Const-Generic-Wrapper darauf leben auf dem Stack. Bei kleinen Größen ein wichtiger Performance-Vorteil gegenüber Heap-basierten Containern.

Aktuell ohne arithmetische Type-Operationen (stable).

[T; N * 2] als Rückgabe-Typ funktioniert in stable Rust noch nicht. Das generic_const_exprs-Feature ist auf Nightly verfügbar und entspannt diese Einschränkung.

Type-Aliase für gängige Werte.

pub type Vec3 = Vector<3> macht den Code lesbarer als die rohe Const-Generic-Form. Klassisches Pattern in Mathematik/Grafik-Bibliotheken.

Wann nicht Const-Generic?

Wenn die Größe dynamisch ist (Vec<T> ist richtig), bei sehr großen Arrays (Box<[T; N]> um Stack-Overflow zu vermeiden), oder wenn die Compile-Zeit-Strenge die Bequemlichkeit nicht wert ist.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Generics

Zur Übersicht