std::numeric_limits y la inspección de tipos numéricos

Cuando escribes código genérico con plantillas, no puedes asumir que T se comporta como un int o un double. Necesitas una forma de preguntar al sistema de tipos: “¿Qué tan grande puede ser este número?”, “¿Tiene decimales?” o “¿Qué tan precisa es la comparación?”. std::numeric_limits<T>, definido en <limits>, es la interfaz estándar para consultar estas propiedades de forma estática y segura para tipos.

A diferencia de las macros de C como INT_MAX o DBL_MAX de <climits> y <cfloat>, que son constantes globales y difíciles de usar en plantillas, std::numeric_limits es una plantilla que permite que tu código sea agnóstico al tipo. Esto es vital en metaprogramación: si diseñas una función para encontrar el valor máximo en un contenedor, no puedes usar INT_MAX si el contenedor es de float. Usarás std::numeric_limits<T>::max().

Sin embargo, no es solo un contenedor de constantes. Es una herramienta de introspección. Te permite saber si un tipo es un entero (is_integer), si tiene signo (is_signed), o si sigue el estándar IEEE 754 (is_iec559). También es crucial para la aritmética de punto flotante: epsilon() te da la resolución mínima, es decir, la distancia entre 1.0 y el siguiente valor representable, algo fundamental para evitar errores de precisión al comparar igualdades.

Debes usarlo siempre que tu lógica dependa de los límites del tipo o de su naturaleza matemática. Si intentas comparar dos flotantes con ==, vas a fallar; si usas epsilon() para definir un umbral de tolerancia, vas a tener un código robusto. El desastre ocurre cuando confundes min() con lowest() en tipos de punto flotante. En enteros, min() es el valor más pequeño; en flotantes, min() es el valor positivo más pequeño (el más cercano al cero), mientras que lowest() te devuelve el valor más negativo posible. Equivocar esto en un algoritmo de optimización de rangos resultará en un error de lógica silencioso que es un dolor de cabeza depurar.

#include <iostream>
#include <limits>
#include <cmath>
#include <type_traits>
#include <iomanip>
#include <string_view>

// Función para inspeccionar las propiedades de un tipo numérico
template <typename T>
void inspeccionar_tipo(std::string_view nombre) {
    std::cout << "\n--- Propiedades de " << nombre << " ---\n";
    std::cout << "Máximo:    " << std::numeric_limits<T>::max() << "\n";
    
    // Cuidado: en floats, min() no es el valor más negativo
    std::cout << "Mínimo (min): " << std::numeric_limits<T>::min() << "\n";
    std::cout << "Mínimo (lowest): " << std::numeric_limits<T>::lowest() << "\n";
    
    if constexpr (std::is_floating_point_v<T>) {
        std::cout << "Epsilon:  " << std::numeric_limits<T>::epsilon() << "\n";
        std::cout << "Infinito: " << std::numeric_limits<T>::infinity() << "\n";
        std::cout << "NaN:      " << std::numeric_limits<T>::quiet_NaN() << "\n";
        std::cout << "IEEE 754: " << std::boolalpha << std::numeric_limits<T>::is_iec559 << "\n";
    } else {
        std::cout << "Es entero: " << std::boolalpha << std::numeric_limits<T>::is_integer << "\n";
    }
    
    std::cout << "Es con signo: " << std::boolalpha << std::numeric_limits<T>::is_signed << "\n";
    std::cout << "Dígitos base: " << std::numeric_limits<T>::digits << "\n";
}

// Comparación segura para tipos de punto flotante
template <typename T>
bool son_casi_iguales(T a, T b) {
    if constexpr (std::is_floating_point_v<T>) {
        // Usamos epsilon para evitar errores de precisión por redondeo
        return std::abs(a - b) <= std::numeric_limits<T>::epsilon();
    } else {
        return a == b;
    }
}

int main() {
    inspeccionar_tipo<int>("int");
    inspeccionar_tipo<double>("double");
    inspeccionar_tipo<float>("float");

    double d1 = 0.1 + 0.2;
    double d2 = 0.3;

    std::cout << "\n--- Prueba de precisión (double) ---\n";
    std::cout << "d1: " << std::setprecision(17) << d1 << "\n";
    std::cout << "d2: " << std::setprecision(17) << d2 << "\n";
    std::cout << "Son iguales (==): " << (d1 == d2 ? "Sí" : "No") << "\n";
    std::cout << "Son casi iguales (epsilon): " << (son_casi_iguales(d1, d2) ? "Sí" : "No") << "\n";

    return 0;
}

Desglose del ejemplo

En la función inspeccionar_tipo<T>, la directiva if constexpr (introducida en [C++17]) es clave. Permite que el compilador descarte el bloque de código que no sea válido para el tipo T durante la fase de compilación. Si pasas un int, el compilador ni siquiera intentará generar el código para std::numeric_limits<int>::epsilon(), lo cual evitaría un error de compilación.

Fíjate en la salida de double y float. Verás que min() devuelve un valor extremadamente pequeño pero positivo (casi cero), mientras que lowest() devuelve un número negativo muy grande. Esto es porque en el estándar IEEE 754, los valores representables se dividen en una zona negativa, un cero y una zona positiva; min() apunta al límite de la zona positiva normalizada.

En son_casi_iguales, aplicamos la lógica de epsilon(). Debido a que la aritmética de punto flotante no es exacta (la suma de 0.1 + 0.2 no es exactamente 0.3 en binario), comparar a == b suele dar false debido a errores de redondeo en el último bit (LSB). Al usar std::abs(a - b) <= std::numeric_limits<T>::epsilon(), estamos preguntando si la diferencia es menor que la resolución mínima que el tipo puede distinguir.

Finalmente, std::numeric_limits<T>::digits nos indica cuántos bits de precisión tienen los números sin el bit de signo. Para un double típico, esto suele ser 53 (52 de mantisa + 1 implícito). Esta información es vital cuando estás implementando algoritmos de serialización o compresión donde necesitas saber exactamente cuántos bits de datos significativos estás moviendo.

El error frecuente

Un error clásico ocurre al intentar inicializar una variable para encontrar el valor mínimo de un conjunto de datos usando std::numeric_limits<T>::min() con tipos float o double.

// ERROR FATAL: Lógica incorrecta para flotantes
float valor_minimo = std::numeric_limits<float>::min(); 
float datos[] = {-10.0f, -5.0f, 2.0f};

for(float d : datos) {
    if (d < valor_minimo) valor_minimo = d;
}
// resultado esperado: -10.0
// resultado real: 3.7e-38 (el valor min positivo)

Como min() para un float devuelve el valor positivo más pequeño representable, cualquier número negativo será menor que él, pero el inicializador fallará si todos los números en tu conjunto de datos son positivos y quieres encontrar el más pequeño. Para inicializar una búsqueda de “valor más pequeño” que debe incluir negativos, la única opción correcta es std::numeric_limits<T>::lowest(). Este error es difícil de detectar porque no produce un crash, sino un resultado numérico incorrecto que se propaga silenciosamente. AddressSanitizer no lo detectará porque no hay corrupción de memoria, solo un error de lógica matemática.

148

Dejar un comentario

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

Scroll al inicio