Los const generics permiten que los valores constantes (como usize, i32, bool o char) se pasen como parámetros a tipos o funciones, integrándose directamente en el sistema de tipos de Rust. A diferencia de los parámetros genéricos tradicionales, que representan tipos (como T), los const generics representan valores específicos que el compilador utiliza para especializar el código durante la compilación.

Esta característica es fundamental para implementar abstracciones de costo cero (zero-cost abstractions). Al conocer el valor constante en tiempo de compilación, el compilador puede optimizar layouts de memoria, desapilar bucles (loop unrolling) y asignar arrays directamente en el stack en lugar del heap. Es la tecnología que permite que la biblioteca estándar de Rust maneje arrays [T; N] de forma genérica sin perder la información de su tamaño.

Se deben usar cuando la estructura de un dato o la firma de una función dependa de un valor numérico fijo que es conocido antes de la ejecución, como las dimensiones de una matriz o el tamaño de un búfer. Un error común al utilizarlos es asumir que el sistema de tipos impedirá errores lógicos de índice; si una función asume que un array tiene al menos dos elementos (arr[0] + arr[1]) pero se instancia con N=1, el programa fallará en tiempo de ejecución a pesar de la potencia de los const generics. Además, actualmente los const generics están limitados a tipos primitivos; no es posible usar float o tipos complejos como parámetros de tipo constantes.

// Definición de una estructura con un parámetro de tipo constante N
struct VectorFijo<T, const N: usize> {
    elementos: [T; N],
}

// Un trait que utiliza un parámetro const genérico para exponer su dimensión
trait Medible {
    fn dimensione() -> usize;
}

// Implementación del trait para cualquier VectorFijo
impl<T, const N: usize> Medible for VectorFijo<T, N> {
    fn dimensione() -> usize {
        N
    }
}

impl<T: Copy, const N: usize> VectorFijo<T, N> {
    // Método constructor que inicializa el array con un valor constante
    fn nuevo(valor: T) -> Self {
        Self {
            elementos: [valor; N],
        }
    }

    // Método que opera sobre los elementos asumiendo un tamaño mínimo
    // El compilador conoce el tamaño de 'elementos' en tiempo de compilación
    fn suma_primeros_dos(&self) -> T
    where
        T: std::ops::Add<Output = T> + Copy,
    {
        // Nota: Si N < 2, esto causará un panic en runtime
        self.elementos[0] + self.elementos[1]
    }
}

fn main() {
    // Instanciación con N = 3
    let v_tres = VectorFijo::nuevo(10);
    
    // Instanciación con N = 2
    let v_dos = VectorFijo::nuevo(5);

    println!("Dimensión v_tres: {}", VectorFijo::<i32, 3>::dimensione());
    println!("Suma v_tres: {}", v_tres.suma_primeros_dos());
    println!("Suma v_dos: {}", v_dos.suma_primeros_dos());
}

Análisis del código

  • struct VectorFijo<T, const N: usize>: Define una estructura genérica donde T es un parámetro de tipo y const N: usize es un parámetro de tipo constante. Esto significa que VectorFijo<i32, 3> y VectorFijo<i32, 4> son tipos completamente distintos para el compilador.
  • trait Medible: Declara un trait que no depende de tipos, sino de un valor constante N, permitiendo que el valor de la constante sea accesible mediante un método.
  • impl<T, const N: usize> Medible for VectorFijo<T, N>: Implementa el trait Medible. Aquí, el cuerpo de dimensione() simplemente devuelve N, permitiendo recuperar el valor constante a través de la interfaz del trait.
  • fn nuevo(valor: T) -> Self: Este constructor utiliza la expresión [valor; N]. Debido a que T implementa Copy, el compilador puede replicar el valor valor N veces para inicializar el array interno elementos.
  • fn suma_primeros_dos(&self) -> T: Esta función demuestra cómo el compilador utiliza el conocimiento del tamaño. Aunque el acceso self.elementos[0] y self.elementos[1] parece dinámico, el compilador sabe exactamente qué offsets está accediendo gracias a que N es parte de la definición del tipo.
  • v_tres y v_dos en main: Se crean dos instancias con diferentes valores de N. Observe cómo VectorFijo::<i32, 3>::dimensione() utiliza la sintaxis de turbofish para especificar el parámetro constante cuando el compilador no puede inferirlo automáticamente en un contexto de tipo estático.

109