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:
struct Container<T> {
wert: T,
}Bei Const-Generics ist der Parameter ein konstanter Wert:
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.
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
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
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.
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.
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 — Vec3 ≠ Vec4.
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
#[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
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
#[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
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
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
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
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
- Rust Reference – Const-Generics
- Rust Blog – Const Generics MVP
- The Rust Book – Generic Data Types
- Tracking Issue – Generic Const Exprs