Ir al contenido

constexpr, consteval y Programacion en Tiempo de Compilacion

Mueve la computacion del tiempo de ejecucion al tiempo de compilacion usando constexpr, consteval y constinit. Construye tablas de busqueda, valida configuraciones y usa contenedores constexpr.

Programacion en Tiempo de Compilacion en C++

C++ te permite de forma unica ejecutar codigo real en tiempo de compilacion. Esto significa que el compilador puede calcular valores, validar entradas, construir tablas de busqueda, e incluso ejecutar algoritmos complejos — todo antes de que tu programa se distribuya. El resultado se incorpora directamente en el binario como constantes.

La evolucion de la programacion en tiempo de compilacion en C++:

- C++11: constexpr introducido — funciones simples, de una sola sentencia de retorno
- C++14: constexpr relajado — bucles, variables locales, multiples sentencias
- C++17: if constexpr — ramificacion en tiempo de compilacion que descarta caminos no tomados
- C++20: consteval (debe ser en tiempo de compilacion), constinit (asegura que la inicializacion estatica sea en tiempo de compilacion), contenedores constexpr (vector, string)
- C++23: if consteval — detecta si estas en un contexto de evaluacion constante

Funciones constexpr

Una funcion constexpr puede evaluarse en tiempo de compilacion o en tiempo de ejecucion. Si se llama con argumentos en tiempo de compilacion en un contexto que requiere una constante (argumento de template, variable constexpr, static_assert, tamano de array), el compilador la evalua en tiempo de compilacion. De lo contrario, se ejecuta en tiempo de ejecucion como una funcion normal.

constexpr_functions.cpp
#include <iostream>
#include <array>
#include <cstdint>

constexpr int factorial(int n) {
    int result = 1;
    for (int i = 2; i <= n; ++i) {
        result *= i;
    }
    return result;
}

constexpr int fibonacci(int n) {
    if (n <= 1) return n;
    int a = 0, b = 1;
    for (int i = 2; i <= n; ++i) {
        int next = a + b;
        a = b;
        b = next;
    }
    return b;
}

int main() {
    // Compile-time evaluation: used in constexpr context
    constexpr int fact10 = factorial(10);  // computed at compile time
    static_assert(fact10 == 3628800);

    constexpr int fib10 = fibonacci(10);   // computed at compile time
    static_assert(fib10 == 55);

    // Compile-time array sizes
    std::array<int, factorial(5)> arr{};   // array of 120 elements

    // Runtime evaluation: argument not constexpr
    int n;
    std::cin >> n;
    std::cout << "factorial(" << n << ") = " << factorial(n) << '\n';
    // Same function, now runs at runtime
}

consteval y constinit (C++20)

consteval marca una funcion que debe evaluarse en tiempo de compilacion — llamarla con valores en tiempo de ejecucion es un error de compilacion. Esto se llama una "funcion inmediata" (immediate function). constinit asegura que una variable con duracion de almacenamiento estatico o thread-local sea inicializada en tiempo de compilacion, previniendo el "fiasco del orden de inicializacion estatica" — pero a diferencia de constexpr, la variable puede modificarse despues de la inicializacion.

consteval_constinit.cpp
#include <iostream>

// consteval: MUST be evaluated at compile time
consteval int square(int x) {
    return x * x;
}

// constinit: must be initialized at compile time,
// but can be modified at runtime
constinit int global_value = square(7);  // 49, computed at compile time

// constexpr variable: initialized at compile time AND immutable
constexpr int fixed_value = square(10);  // 100, compile time, cannot change

int main() {
    constexpr int a = square(5);  // OK: compile-time context
    static_assert(a == 25);

    // int x = 5;
    // int b = square(x);  // ERROR: consteval requires compile-time args

    // constinit variable can be modified after initialization
    global_value = 100;  // OK: runtime modification allowed
    std::cout << global_value << '\n'; // 100

    // fixed_value = 200;  // ERROR: constexpr variable is const
}

if constexpr en la Practica

if constexpr (C++17) evalua una condicion en tiempo de compilacion y descarta la rama no tomada completamente. La rama descartada no se instancia — no necesita ser codigo valido para los argumentos de template dados. Esto es fundamentalmente diferente de un if regular, donde ambas ramas deben ser validas.

Reglas clave:

1. La condicion debe ser una expresion constante en tiempo de compilacion.
2. En un template, la rama descartada no se instancia para los argumentos de template actuales.
3. Fuera de un template, ambas ramas deben ser sintacticamente validas (simplemente no se ejecutaran).
4. if constexpr puede aparecer en funciones regulares, no solo en templates — pero el descarte solo se aplica en la instanciacion de templates.
5. Cada rama de if constexpr tiene su propio ambito — las variables declaradas en una rama no son visibles en la otra.

Contenedores constexpr (C++20)

C++20 permite usar std::vector y std::string en contextos constexpr. Puedes construir, manipular y consultar contenedores en tiempo de compilacion — una expansion masiva de las capacidades de programacion en tiempo de compilacion. La restriccion: asignacion transitoria (transient allocation) — la memoria asignada durante la evaluacion en tiempo de compilacion debe liberarse antes de que la evaluacion constexpr termine.

constexpr_containers.cpp
#include <vector>
#include <string>
#include <algorithm>
#include <numeric>
#include <array>

// Build and process a vector at compile time (C++20)
constexpr int sum_of_squares(int n) {
    std::vector<int> v;
    for (int i = 1; i <= n; ++i) {
        v.push_back(i * i);
    }
    return std::accumulate(v.begin(), v.end(), 0);
    // vector memory freed here — transient allocation
}

// Compile-time string processing
constexpr std::size_t count_vowels(std::string_view sv) {
    std::size_t count = 0;
    for (char c : sv) {
        if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u' ||
            c == 'A' || c == 'E' || c == 'I' || c == 'O' || c == 'U') {
            ++count;
        }
    }
    return count;
}

// Generate a lookup table at compile time
template <std::size_t N>
constexpr auto generate_squares() {
    std::array<int, N> result{};
    for (std::size_t i = 0; i < N; ++i) {
        result[i] = static_cast<int>(i * i);
    }
    return result;
}

static_assert(sum_of_squares(5) == 55); // 1+4+9+16+25
static_assert(count_vowels("Hello World") == 3);

constexpr auto squares = generate_squares<10>();
static_assert(squares[7] == 49);

Programacion en Tiempo de Compilacion Practica

La computacion en tiempo de compilacion no es solo un truco de salon. Los usos del mundo real incluyen: construir tablas de busqueda incorporadas en el binario, validar configuraciones en tiempo de compilacion, calcular hashes para coincidencia de strings en tiempo de compilacion, y generar rutas de codigo optimizadas.

practical_constexpr.cpp
#include <array>
#include <cstdint>
#include <stdexcept>

// Compile-time CRC32 table generation
constexpr auto generate_crc32_table() {
    std::array<uint32_t, 256> table{};
    for (uint32_t i = 0; i < 256; ++i) {
        uint32_t crc = i;
        for (int j = 0; j < 8; ++j) {
            crc = (crc >> 1) ^ (0xEDB88320 * (crc & 1));
        }
        table[i] = crc;
    }
    return table;
}

constexpr auto crc32_table = generate_crc32_table();

// Compile-time configuration validation
struct Config {
    int port;
    int max_connections;
    int timeout_ms;
};

consteval Config validated_config(int port, int max_conn, int timeout) {
    if (port < 1 || port > 65535)
        throw "Port out of range";  // compile error if invalid!
    if (max_conn < 1 || max_conn > 10000)
        throw "Invalid max connections";
    if (timeout < 0)
        throw "Negative timeout";
    return {port, max_conn, timeout};
}

// This validates at compile time — typo or bad value = build fails
constexpr auto config = validated_config(8080, 1000, 5000); // OK
// constexpr auto bad = validated_config(99999, 1000, 5000); // ERROR!
Error común

Errores comunes con la programacion en tiempo de compilacion:

1. Asumir que constexpr significa solo tiempo de compilacion — una funcion constexpr tambien puede ejecutarse en tiempo de ejecucion. Solo consteval garantiza la evaluacion en tiempo de compilacion.
2. Asignacion transitoria — en contextos constexpr de C++20, toda la memoria asignada durante la evaluacion debe liberarse antes de que la evaluacion se complete. No puedes almacenar un constexpr std::vector como global (su memoria del heap persistiria).
3. No todas las funciones son constexpr-safe — E/S, creacion de hilos, reinterpret_cast, y ensamblador inline no pueden aparecer en contextos constexpr.
4. Uso excesivo de constexpr — computar todo en tiempo de compilacion aumenta los tiempos de compilacion. Reservalo para valores que genuinamente se benefician de ser constantes en tiempo de compilacion.

Puntos clave
  • Las funciones constexpr se ejecutan en tiempo de compilacion O en tiempo de ejecucion dependiendo del contexto
  • consteval (C++20) fuerza la evaluacion solo en tiempo de compilacion — los argumentos en tiempo de ejecucion son errores de compilacion
  • constinit (C++20) asegura la inicializacion en tiempo de compilacion de variables estaticas/thread-local pero permite modificacion en tiempo de ejecucion
  • if constexpr descarta la rama no tomada, que no necesita ser valida para los argumentos de template dados
  • C++20 permite std::vector y std::string en contextos constexpr con asignacion transitoria
  • Los usos practicos incluyen generacion de tablas de busqueda, validacion de configuracion y hashing en tiempo de compilacion

Quiz — Pon a prueba tus conocimientos

(15 XP)

1. Cual es la diferencia entre `constexpr` y `consteval`?

2. Puedes tener un `constexpr std::vector<int>` como variable global en C++20?

3. Que garantiza `constinit`?