Avanzado

Capítulo 11: Auto, Const, Constexpr y Static

C++ moderno incluye características poderosas para escribir código más seguro, eficiente y expresivo. En este capítulo exploraremos la deducción automática de tipos, constantes en tiempo de compilación, y el comportamiento especial de las variables estáticas.

Objetivos

Deducción de tipos con auto

La palabra clave auto permite que el compilador deduzca automáticamente el tipo de una variable basándose en su inicializador. Esto hace el código más limpio y mantenible.

// Antes de auto
std::vector<std::string>::iterator it = myVector.begin();

// Con auto - mucho más limpio
auto it = myVector.begin();

// auto deduce el tipo del resultado
auto sum = 10 + 20.5;        // double (promoción automática)
auto value = 42;              // int
auto name = "Claude";         // const char*
auto pi = 3.14f;             // float

AAA: Almost Always Auto

Muchos expertos en C++ recomiendan el principio AAA (Almost Always Auto) . Esto sigue el principio DRY (Don't Repeat Yourself) y hace el código más resistente a cambios.

// Sin auto - repetitivo y propenso a errores
std::map<std::string, std::vector<int>> myMap;
std::map<std::string, std::vector<int>>::iterator it = myMap.begin();

// Con auto - limpio y mantenible
auto myMap = std::map<std::string, std::vector<int>>{};
auto it = myMap.begin();

// Uniform initialization con auto
auto x = int{42};           // Inicialización uniforme
auto y = int{};            // Valor por defecto (0)
auto vec = std::vector<int>{1, 2, 3, 4, 5};
Cuidado con las conversiones implícitas

Las conversiones implícitas pueden causar bugs sutiles. Con auto y conversiones explícitas, el código es más claro sobre las intenciones:

float a = 10.5;
int b = a;  // ¿Bug o intencional? No está claro

// Mejor - intención clara
auto c = static_cast<int>(a);  // Conversión explícita

Const: Inmutabilidad en tiempo de ejecución

const marca variables como de solo lectura después de su inicialización. Es fundamental para escribir código seguro y expresar intenciones claramente.

const int MAX_USERS = 100;     // No puede cambiar
const auto pi = 3.14159;       // auto con const

void processUser(const User& user) {
    // user es de solo lectura, no se puede modificar
    // Pasar por referencia const evita copias innecesarias
}

// const con punteros
const int* ptr1;        // Puntero a int constante (el valor no cambia)
int* const ptr2;        // Puntero constante a int (el puntero no cambia)
const int* const ptr3;  // Todo es constante

Constexpr: Computación en tiempo de compilación

constexpr indica que algo puede ser evaluado en tiempo de compilación . Esto permite optimizaciones significativas y garantías más fuertes que const.

// Función constexpr - puede ejecutarse en tiempo de compilación
constexpr std::uint64_t factorial(const std::uint8_t n) {
    std::uint64_t result = 1;
    for (std::uint8_t i = 1; i <= n; ++i) {
        result *= i;
    }
    return result;
}

int main() {
    // const - valor constante, puede ser runtime
    const auto runtime_result = factorial(5);

    // constexpr - DEBE ser compile-time
    constexpr auto compile_time_result = factorial(5);  // 120 calculado al compilar

    // Esto permite usarlo en contextos compile-time
    std::array<int, factorial(4)> myArray;  // Array de tamaño 24
}
¿Cuándo usar const vs constexpr? Intermedio
  • const: Para valores que no cambian después de inicialización, pero pueden ser calculados en runtime
  • constexpr: Para valores que PUEDEN y DEBERÍAN ser calculados en compile-time
  • constexpr implica const: Una variable constexpr es automáticamente const
  • C++17 y posterior: constexpr se ha vuelto mucho más poderoso, permitiendo if constexpr y más
¿Por qué constexpr auto result = getUserInput(); no compilaría?
Respuesta:

Porque getUserInput() requiere entrada del usuario en tiempo de ejecución, y constexpr requiere que el valor sea conocido en tiempo de compilación. Las variables constexpr solo pueden inicializarse con expresiones que el compilador puede evaluar completamente.

Static: Variables con duración de almacenamiento estático

static tiene varios significados en C++, pero para variables locales significa que la variable persiste entre llamadas a la función.

void countCalls() {
    static int counter = 0;  // Se inicializa SOLO la primera vez
    ++counter;
    std::cout << "Llamada número: " << counter << "\n";
}

int main() {
    countCalls();  // Imprime: Llamada número: 1
    countCalls();  // Imprime: Llamada número: 2
    countCalls();  // Imprime: Llamada número: 3
    // counter mantiene su valor entre llamadas
}

// Ejemplo práctico: Singleton pattern simplificado
Database& getDatabase() {
    static Database db("connection_string");  // Creado una sola vez
    return db;  // Siempre retorna la misma instancia
}
Thread Safety con static

Desde C++11, la inicialización de variables static locales es thread-safe. El compilador garantiza que se inicialicen exactamente una vez, incluso con múltiples threads.

Contextos de static

Combinando conceptos

Estos conceptos se pueden combinar para escribir código más eficiente y expresivo:

// Template con auto, const y constexpr
template<typename T>
constexpr auto max(const T& a, const T& b) {
    return (a > b) ? a : b;
}

// Función con cache usando static
auto fibonacci(int n) {
    static std::map<int, long long> cache;

    if (auto it = cache.find(n); it != cache.end()) {
        return it->second;  // Ya calculado
    }

    if (n <= 1) return n;

    auto result = fibonacci(n-1) + fibonacci(n-2);
    cache[n] = result;  // Guardar en cache
    return result;
}

// Constexpr if (C++17) para decisiones en compile-time
template<typename T>
auto processValue(T value) {
    if constexpr (std::is_integral_v<T>) {
        return value * 2;  // Para enteros
    } else {
        return value / 2;  // Para flotantes
    }
}

Resumen