значения как параметры дженериковvalues as generic parameters

Const genericsConst generics

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.

стабилизировано в Rust 1.51 (2021)stabilized in Rust 1.51 (2021) частый вопрос: зачем нужны const generics и что они чинятcommon question: why const generics exist and what they fix

Что этоWhat it is

Помимо типового параметра дженерика можно объявить параметр-значение: 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.

до const generics — по impl на каждый размерbefore const generics — one impl per size
// 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
с const generics — одно определение на любой Nwith const generics — one definition for any N
struct Buffer<const N: usize> {
    data: [u8; N],
}
// Buffer<4> и Buffer<16> — генерируются из одного определения// Buffer<4> and Buffer<16> are generated from one definition

Зачем ввелиWhy it was introduced

До 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:

01

Не было стек-выделяемой альтернативы Vec<T>There was no stack-allocated alternative to Vec<T>

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.

02

Численные/embedded библиотеки хотели Vector<f32, 3> и Vector<f32, 4> как различные типыNumerics/embedded libraries wanted Vector<f32, 3> and Vector<f32, 4> as distinct types

Небольшие библиотеки векторной/матричной математики хотели, чтобы разные размерности были действительно различными типами без единого выделения на куче, а не одним рантайм-параметризованным типом с полем размера.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.

03

Система типов теперь несёт проверенный размер так же точно, как типThe type system now carries a checked size as precisely as it carries a type

Const generics закрывают этот разрыв: размер, известный на этапе компиляции, становится частью типа — точно так же, как тип элемента.Const generics close this gap: a compile-time-known size becomes part of the type — exactly as precisely as the element type.

Как это работает под капотомHow it works under the hood

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

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Стек-выделяемый Vector<const N: usize> для векторной математикиA stack-allocated Vector<const N: usize> for small vector math

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
02

RingBuffer<T, const CAPACITY: usize> с ёмкостью в самом типеA RingBuffer<T, const CAPACITY: usize> with capacity baked into the type

Два кольцевых буфера разной ёмкости — просто разные, несовместимые типы; несовпадение ёмкости ловится на этапе компиляции, а не рантайм-ассертом.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
03

Функция, обобщённая по длине массива, без арифметики над NA function generic over array length, with no arithmetic on N

Более сложные 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
}
04

Историческая мотивация: макросы для [T; 0]..[T; 32]Historical motivation: macros for [T; 0]..[T; 32]

До стабилизации 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 */ }

ГраблиGotchas

Const generic параметры ограничены простыми типамиConst generic parameters are restricted to simple types

На стабильном 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 выражения пока незрелыComplex const-generic expressions remain immature

Арифметика над 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.

Разные N — полностью несвязанные типы без автоконверсииDifferent N values are completely unrelated types with no auto-conversion

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.