значения как параметры дженериковvalues as generic parameters
Const generics позволяют использовать ЗНАЧЕНИЕ (не только тип) в качестве параметра дженерика — чаще всего известное на этапе компиляции целое число, параметризующее размер массивоподобной структуры: struct Buffer<const N: usize> { data: [u8; N] }. До стабилизации этой возможности (Rust 1.51, 2021) размер массива N по сути не мог быть обобщённым параметром в большинстве полезных контекстов.Const generics let a VALUE (not just a type) be a generic parameter — most commonly a compile-time-known integer parametrizing an array-like size: struct Buffer<const N: usize> { data: [u8; N] }. Before this feature stabilized (Rust 1.51, 2021), array size N essentially couldn't be a generic parameter in most useful contexts.
Помимо типового параметра дженерика можно объявить параметр-значение: struct Buffer<const N: usize> { data: [u8; N] } или самостоятельная функция fn first_n<const N: usize>(arr: &[i32]) -> [i32; N]. Канонический мотивирующий случай — [T; N] (массив фиксированного размера): до const generics стандартная библиотека и многие крейты реализовывали трейты вроде Default/Debug для массивов [T; 0] вплоть до [T; 32] вручную, макросом, потому что не было способа сказать "для любого N" обобщённо.Besides a type parameter, a generic can declare a value parameter: struct Buffer<const N: usize> { data: [u8; N] }, or a standalone function fn first_n<const N: usize>(arr: &[i32]) -> [i32; N]. The canonical motivating case is [T; N] (a fixed-size array): before const generics, the standard library and many crates implemented traits like Default/Debug for arrays [T; 0] through [T; 32] by hand, via a macro, because there was no way to say "for all N" generically.
// macro-сгенерированные повторяющиеся impl, вручную до размера 32// macro-generated repetitive impls, by hand up to size 32
impl Default for [T; 0] { /* ... *//* ... */ }
impl Default for [T; 1] { /* ... *//* ... */ }
// ... и так далее вплоть до [T; 32], "для любого N" выразить было нельзя// ... and so on up to [T; 32] — "for any N" simply wasn't expressible
struct Buffer<const N: usize> {
data: [u8; N],
}
// Buffer<4> и Buffer<16> — генерируются из одного определения// Buffer<4> and Buffer<16> are generated from one definition
До const generics система типов не могла выразить "массив фиксированного размера N" как единое обобщённое определение:Before const generics, the type system had no way to express "a fixed-size array of generic length N" as a single generic definition:
struct Matrix<T> могла хранить Vec<T> (куча, размер во время выполнения, аллокация), но не имела нулевой по аллокациям, стек-выделяемой альтернативы с размером, проверяемым на этапе компиляции — реальная потребность в embedded и численно-интенсивном коде.struct Matrix<T> could hold a Vec<T> (heap, runtime-sized, an allocation), but had no zero-allocation, stack-allocated alternative with a compile-time-checked size — a real need in embedded and numerics-heavy code.
Небольшие библиотеки векторной/матричной математики хотели, чтобы разные размерности были действительно различными типами без единого выделения на куче, а не одним рантайм-параметризованным типом с полем размера.Small vector/matrix math libraries wanted different dimensions to be genuinely distinct types with zero heap allocation involved, not one runtime-parametrized type carrying a size field.
Const generics закрывают этот разрыв: размер, известный на этапе компиляции, становится частью типа — точно так же, как тип элемента.Const generics close this gap: a compile-time-known size becomes part of the type — exactly as precisely as the element type.
Matrix<3, 3> и Matrix<4, 4> — действительно разные типы, а не один обобщённый тип с полем размера, известным только во время выполнения:Matrix<3, 3> and Matrix<4, 4> are genuinely distinct types, not one generic type with a runtime-known size field:
struct Matrix<const ROWS: usize, const COLS: usize> {
data: [[f64; COLS]; ROWS],
}
let m3: Matrix<3, 3> = Matrix { data: [[0.0; 3]; 3] };
let m4: Matrix<4, 4> = Matrix { data: [[0.0; 4]; 4] };
// m3 и m4 — несовместимые типы, компилятор не даст присвоить одно другому// m3 and m4 are incompatible types — the compiler won't let one be assigned to the other
Функция тоже может быть обобщена по длине массива:A function can likewise be generic over an array's length:
fn sum_array<const N: usize>(arr: [i32; N]) -> i32 {
arr.iter().sum()
}
sum_array([1, 2, 3]); // N выведен как 3// N inferred as 3
sum_array([1, 2, 3, 4]); // N выведен как 4 — другая инстанциация// N inferred as 4 — a different instantiation
2D/3D/4D-векторы без единого выделения на куче — размер проверен на этапе компиляции, в отличие от Vec<f64>, где размер известен только во время выполнения и данные лежат в куче.2D/3D/4D vectors with zero heap allocation — size checked at compile time, unlike Vec<f64>, where the size is only known at runtime and the data lives on the heap.
struct Vector<const N: usize> { data: [f64; N] }
impl<const N: usize> Vector<N> {
fn dot(&self, other: &Self) -> f64 {
self.data.iter().zip(other.data.iter()).map(|(a, b)| a * b).sum()
}
}
let v3: Vector<3> = Vector { data: [1.0, 2.0, 3.0] }; // на стеке, нет аллокации// on the stack, no allocation
let heap_alt: Vec<f64> = vec![1.0, 2.0, 3.0]; // на куче, размер во время выполнения// on the heap, runtime-sized
Два кольцевых буфера разной ёмкости — просто разные, несовместимые типы; несовпадение ёмкости ловится на этапе компиляции, а не рантайм-ассертом.Two ring buffers of different capacities are simply different, incompatible types — a capacity mismatch is caught at compile time rather than by a runtime assertion.
struct RingBuffer<T, const CAPACITY: usize> {
data: [Option<T>; CAPACITY],
head: usize,
}
fn merge(a: RingBuffer<i32, 16>, b: RingBuffer<i32, 16>) { /* ... *//* ... */ }
// merge(buf16, buf32) — ОШИБКА КОМПИЛЯЦИИ: разные типы, не просто разные значения поля// merge(buf16, buf32) — COMPILE ERROR: different types, not just different field values
Более сложные const-generic выражения (например, границы вида where [(); N - 1]:) остаются менее зрелой, местами nightly-областью — этот пример намеренно ограничен тем, что стабильно и надёжно работает уже сейчас.More advanced const-generic expressions (e.g. bounds like where [(); N - 1]:) remain a less mature, sometimes nightly-adjacent area — this example is deliberately limited to what works reliably on stable Rust today.
fn first_element<const N: usize>(arr: [i32; N]) -> Option<i32> {
arr.get(0).copied() // работает для любого N, никакой арифметики над N в границах// works for any N, no arithmetic on N in the bounds
}
До стабилизации const generics в Rust 1.51 (март 2021, объявлено в блоге Rust) стандартная библиотека буквально генерировала повторяющиеся impl для массивов размером от 0 до 32 через макрос — реальный, задокументированный исторический факт, а не гипотетический пример.Before const generics stabilized in Rust 1.51 (March 2021, announced on the Rust blog), the standard library literally macro-generated repetitive impls for array sizes 0 through 32 — a real, documented historical fact, not a hypothetical example.
// упрощённая иллюстрация подхода до 1.51// a simplified illustration of the pre-1.51 approach
macro_rules! array_impls {
($($N:expr)+) => {
$(impl<T: Default> Default for [T; $N] { /* ... *//* ... */ })+
}
}
array_impls! { 0 1 2 3 /* ... и так далее вплоть до 32 *//* ... and so on up to 32 */ }
На стабильном Rust параметром-значением может быть в основном целочисленный тип, bool или char — использовать произвольную структуру или число с плавающей точкой как const generic параметр без nightly-фич нельзя, что порой удивляет, учитывая внешнюю гибкость этой возможности.On stable Rust, a value parameter can mainly be an integer type, bool, or char — using an arbitrary struct or a floating-point value as a const generic parameter isn't possible without nightly features, which is sometimes surprising given how flexible const generics might initially seem.
Арифметика над const generic параметрами в границах или размерах ассоциированных массивов (например, доказательство корректности [T; N-1]) остаётся ограниченной и местами нестабильной — не стоит считать, что произвольная арифметика над const generics "просто работает", как в зависимо типизированном языке; это развивающаяся область.Arithmetic on const generic parameters in bounds or associated array sizes (e.g. proving [T; N-1] is valid) remains limited and unstable in various forms — don't assume arbitrary const-generic arithmetic "just works" the way it might in a dependently-typed language; this is an evolving area.
Matrix<3, 3> и Matrix<4, 4> не имеют общего супертипа и никакого автоматического приведения друг к другу. Коду, которому нужна обобщённость по самому размеру (а не фиксированный конкретный размер), приходится протаскивать const generic параметр везде явно — это может быть многословнее, чем рантайм-размерная альтернатива.Matrix<3, 3> and Matrix<4, 4> share no common supertype and no automatic coercion between them. Code that needs to be generic OVER the size itself (not for one fixed size) still has to thread the const generic parameter through explicitly everywhere — which can be more verbose than a runtime-sized alternative.