Const generics en Rust: parámetros de tipo que son valores constantes

Los genéricos clásicos de Rust parametrizan sobre tipos: Vec<T> funciona con cualquier tipo T. Los const generics amplían esto: permiten parametrizar sobre valores constantes en tiempo de compilación. El caso más obvio son los arrays de tamaño fijo.

Antes de Rust 1.51 (que los estabilizó), escribir código genérico sobre arrays era muy limitado. Tenías que implementar manualmente cada tamaño. Con const generics:

fn suma_array<const N: usize>(arr: &[i32; N]) -> i32 {
    arr.iter().sum()
}

let a = [1, 2, 3];
let b = [1, 2, 3, 4, 5];

println!("{}", suma_array(&a));  // 6
println!("{}", suma_array(&b));  // 15

La función compila una versión distinta para cada valor de N que se use. Es monomorfización, igual que con los genéricos de tipo.

Structs con tamaño fijo parametrizado

El caso más común es una struct que contiene un array de tamaño fijo:

struct VectorFijo<T, const N: usize> {
    datos: [T; N],
}

impl<T: Copy + Default, const N: usize> VectorFijo<T, N> {
    pub fn nuevo() -> Self {
        VectorFijo { datos: [T::default(); N] }
    }

    pub fn longitud(&self) -> usize {
        N
    }

    pub fn get(&self, idx: usize) -> Option<&T> {
        self.datos.get(idx)
    }
}

impl<T: std::ops::Add<Output = T> + Copy, const N: usize> VectorFijo<T, N>
{
    pub fn suma(&self, otro: &VectorFijo<T, N>) -> VectorFijo<T, N> {
        let mut resultado = self.datos;
        for i in 0..N {
            resultado[i] = self.datos[i] + otro.datos[i];
        }
        VectorFijo { datos: resultado }
    }
}

let v1: VectorFijo<f64, 3> = VectorFijo { datos: [1.0, 2.0, 3.0] };
let v2: VectorFijo<f64, 3> = VectorFijo { datos: [4.0, 5.0, 6.0] };
let v3 = v1.suma(&v2);

// ERROR en compilación: no puedes sumar vectores de distinto tamaño
// v1.suma(&VectorFijo::<f64, 4> { datos: [1.0, 2.0, 3.0, 4.0] });

La suma de dos VectorFijo<f64, 3> y un VectorFijo<f64, 4> es un error en compilación, no en runtime. El compilador garantiza que solo sumas vectores del mismo tamaño.

Implementar traits para arrays de cualquier longitud

Antes de los const generics, para implementar un trait en [T; N] para múltiples valores de N había que hacerlo manualmente para cada tamaño (y la biblioteca estándar de hecho lo hacía para N de 0 a 32). Con const generics es una sola implementación:

trait Suma {
    fn suma(&self) -> i32;
}

impl<const N: usize> Suma for [i32; N] {
    fn suma(&self) -> i32 {
        self.iter().sum()
    }
}

[1, 2, 3].suma()           // 6
[1, 2, 3, 4, 5].suma()    // 15
[42].suma()                // 42

Const generics en la biblioteca estándar

Rust usa const generics internamente para los arrays. El trait Default ahora está implementado para [T; N] con cualquier N, no solo para N <= 32 como antes:

let arr: [i32; 100] = [0; 100];  // Funciona con cualquier N

// SIMD con tipos de ancho fijo
use std::simd::Simd;
let v: Simd<f32, 4> = Simd::from_array([1.0, 2.0, 3.0, 4.0]);

Tipos válidos como const params

No todos los tipos se pueden usar como const generic. En Rust estable, los tipos permitidos son:

  • Enteros: u8, u16, u32, u64, u128, usize, i8, i16, i32, i64, i128, isize
  • bool
  • char

Los tipos flotantes (f32, f64), strings y structs no están disponibles como const params en Rust estable (aunque están en desarrollo en nightly).

// Válido: usize, bool, char
struct Buffer<const SIZE: usize> { datos: [u8; SIZE] }
struct CondicionalLog<const LOG: bool> { /* ... */ }
struct Caracter<const C: char> { /* ... */ }

// No válido en stable:
// struct Config<const NOMBRE: &'static str> { /* ... */ }  // Solo en nightly

Un ejemplo práctico: matrices tipadas

#[derive(Debug, Clone, Copy, PartialEq)]
struct Matriz<const FILAS: usize, const COLS: usize> {
    datos: [[f64; COLS]; FILAS],
}

impl<const FILAS: usize, const COLS: usize> Matriz<FILAS, COLS> {
    pub fn nueva() -> Self {
        Matriz { datos: [[0.0; COLS]; FILAS] }
    }

    pub fn transponer(&self) -> Matriz<COLS, FILAS> {
        let mut resultado = Matriz::<COLS, FILAS>::nueva();
        for i in 0..FILAS {
            for j in 0..COLS {
                resultado.datos[j][i] = self.datos[i][j];
            }
        }
        resultado
    }
}

// Multiplicación: (FILAS x K) * (K x COLS) = (FILAS x COLS)
impl<const FILAS: usize, const K: usize, const COLS: usize>
    std::ops::Mul<Matriz<K, COLS>> for Matriz<FILAS, K>
{
    type Output = Matriz<FILAS, COLS>;

    fn mul(self, otra: Matriz<K, COLS>) -> Matriz<FILAS, COLS> {
        let mut resultado = Matriz::nueva();
        for i in 0..FILAS {
            for j in 0..COLS {
                for k in 0..K {
                    resultado.datos[i][j] += self.datos[i][k] * otra.datos[k][j];
                }
            }
        }
        resultado
    }
}

let a: Matriz<2, 3> = Matriz::nueva();
let b: Matriz<3, 4> = Matriz::nueva();
let c: Matriz<2, 4> = a * b;  // El compilador verifica que las dimensiones son compatibles

// Error en compilación: 2x3 * 2x4 no es una multiplicación válida
// let err = a * Matriz::<2, 4>::nueva();

Limitaciones actuales

Los const generics en Rust stable aún tienen limitaciones. Las expresiones en const params son limitadas: no puedes hacer aritmética directamente sobre los params en todas las posiciones:

// Esto funciona
fn duplicar<const N: usize>(arr: [i32; N]) -> [i32; N] { arr }

// Esto NO funciona en stable: necesita la feature const_generic_exprs (nightly)
// fn concatenar<const N: usize, const M: usize>(
//     a: [i32; N], b: [i32; M]
// ) -> [i32; N + M] { /* ... */ }

La feature generic_const_exprs en nightly permite expresiones más complejas como N + M en posición de tipo, pero aún no está estabilizada. Para la mayoría de casos prácticos, los const generics estables son suficientes.

Los const generics son la base sobre la que se construyen abstracciones de alto rendimiento sin coste dinámico: tipos de SIMD, buffers de red con tamaños fijos, matrices lineales y cualquier estructura donde el tamaño sea conocido en compilación.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP