Const vs Constexpr: la clave de la inmutabilidad

Cuando defines una variable con const, le estás haciendo una promesa al compilador: “no voy a cambiar el valor de esta etiqueta a través de este nombre”. Sin embargo, const no garantiza que el valor sea inmutable de forma absoluta; es una restricción de acceso para el nombre dado, no necesariamente para la ubicación de memoria. Por el contrario, constexpr es una promesa mucho más fuerte: garantiza que el valor se calculará durante la compilación.

Para entenderlo, piensa en la diferencia entre una instrucción de “solo lectura” y un valor que ya está escrito en el código fuente antes de que el programa empiece a ejecutarse. Usamos const para valores que sabemos que no cambiarán durante la ejecución (como la configuración de una sesión), y usamos constexpr cuando el valor es conocido de antemano y queremos que el compilador lo resuelva para ahorrar tiempo de ejecución o para usarlo en contextos que requieren constantes puras, como el tamaño de un array estático. Si intentas usar un const que depende de un valor de entrada del usuario como si fuera una constante de compilación, el compilador fallará porque no puede predecir el futuro.

Para leer correctamente los tipos con punteros, aplica la regla del reloj (lee de derecha a izquierda):
* int * const p: “p es un puntero constante a un entero”. La dirección no cambia, pero el entero sí.
* const int * p: “p es un puntero a un entero constante”. La dirección puede cambiar, pero el entero no a través de p.
* const int * const p: “p es un puntero constante a un entero constante”. Nada puede cambiar.

#include <iostream>
#include <array>

// En C++20, constexpr es extremadamente potente.
// Esta función se evalúa en tiempo de compilación.
constexpr int calcular_factorial(int n) {
    return (n <= 1) ? 1 : n * calcular_factorial(n - 1);
}

// Una estructura para demostrar la inmutabilidad de los miembros
struct Config {
    const int id;
    const int version;

    Config(int i, int v) : id(i), version(v) {}
};

int main() {
    // 1. constexpr: El valor es conocido al compilar.
    // Se puede usar para definir tamaños de arrays en el stack.
    constexpr int LIMITE_MAXIMO = 10;
    std::array<int, LIMITE_MAXIMO> datos = {1, 2, 3}; 

    // 2. const: El valor se define en tiempo de ejecución pero no cambia.
    int entrada_usuario;
    std::cout << "Introduce un valor: ";
    std::cin >> entrada_usuario;
    const int valor_fijo = entrada_usuario; // 'valor_fijo' es const, pero su valor depende de runtime.

    // 3. Punteros y const
    int variable_real = 100;
    int otra_variable = 200;

    const int* ptr_a_constante = &variable_real; // El entero es constante a través de este puntero
    int* const ptr_constante = &variable_real;  // El puntero es constante (no puede apuntar a otra cosa)

    // Intentos de modificación:
    // *ptr_a_constante = 50; // ERROR: No puedes cambiar el valor a través de este puntero.
    ptr_constante = &otra_variable; // ERROR: No puedes cambiar la dirección que guarda este puntero.

    // 4. Uso de constexpr para lógica de compilación
    // El compilador ya sabe que el resultado es 120 sin ejecutar el código.
    constexpr int factorial_5 = calcular_factorial(5);

    std::cout << "Factorial de 5: " << factorial_5 << "\n";
    std::cout << "Valor fijo: " << valor_fijo << "\n";
    std::cout << "Dirección de variable_real: " << &variable_real << "\n";

    return 0;
}

Análisis del código

Fíjate en la función calcular_factorial. Al marcarla como constexpr, le permites al compilador ejecutarla durante la fase de compilación. Cuando escribimos constexpr int factorial_5 = calcular_factorial(5);, el compilador no genera código para calcular el factorial cuando el usuario ejecute el programa; simplemente reemplaza esa expresión con el valor 120. Esto es extremadamente eficiente.

En std::array<int, LIMITE_MAXIMO>, LIMITE_MAXIMO debe ser una constante de compilación. Si intentáramos usar valor_fijo (que es const pero su valor depende de std::cin), el compilador lanzaría un error porque std::array necesita saber su tamaño antes de que el programa empiece a correr.

Observa la distinción en los punteros. Con ptr_a_constante, la restricción recae sobre el contenido (el int). Si intentas hacer *ptr_a_constante = 50;, el compilador te detendrá porque has roto la promesa de no modificar el entero a través de ese puntero. Con ptr_constante, la restricción recae sobre la dirección de memoria. Puedes cambiar a qué variable apunta (ptr_constante = &otra_variable), pero no puedes cambiar el contenido de la dirección original a través de él si fuera const int* const.

El error frecuente

Un error muy común es confundir una variable const con una constante de compilación (constexpr).

int x;
std::cin >> x;
const int y = x; // 'y' es constante, su valor no cambiará durante la ejecución.

// ERROR DE COMPILACIÓN:
// El tamaño de un array debe ser una expresión constante.
// Aunque 'y' es const, su valor se conoce hasta que el programa se esté ejecutando.
int mi_array[y]; 

Este error suele pasar desapercibido en lenguajes como Python, pero en C++ es vital. Un const es una promesa de no escritura (read-only), mientras que constexpr es una promesa de disponibilidad inmediata para el compilador. Si necesitas que algo sea usado para definir tipos, tamaños de arrays o como argumentos en static_assert, no basta con usar const; debes usar constexpr.

12

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio