Los tipos de punto flotante (float, double, long double) son tipos de datos diseñados para representar números reales (con decimales) en un formato que permite manejar rangos inmensos, desde valores casi cero hasta números astronómicos. No son valores exactos, sino aproximaciones basadas en el estándar IEEE 754, que divide el número en tres componentes: el signo, el exponente (que indica la escala) y la mantisa o fracción (que contiene los dígitos significativos).
Funciona de este modo porque una computadora tiene una cantidad finita de bits para almacenar un número. Si intentáramos representar todos los decimales posibles, necesitaríamos memoria infinita; al usar un exponente, permitimos que la “coma” se desplace, ganando un rango enorme a cambio de perder precisión en los dígitos menos significativos. Además, la base binaria de las computadoras no puede representar de forma exacta fracciones comunes como $1/10$ ($0.1$), lo que genera errores de redondeo inevitables en el nivel de hardware.
Deberías usar float si necesitas ahorrar memoria en grandes buffers de datos (como en motores de renderizado) y double para la lógica general de cálculo por su mayor precisión. Usa long double solo cuando la precisión sea crítica y la arquitectura lo soporte. Sin embargo, si intentas comparar estos tipos usando la igualdad directa (==), tu lógica fallará de forma errática debido a esos micro-errores de redondeo, y si realizas demasiadas operaciones sucesivas, el error de precisión se acumulará hasta que el valor sea totalmente errático.
#include <iostream>
#include <cmath>
#include <limits>
#include <iomanip>
int main() {
// 1. El problema de la precisión y el error de redondeo
// En binario, 0.1 no es exacto. Es una fracción infinita periódica.
double a = 0.1;
double b = 0.2;
double suma = a + b;
// Configuramos la salida para ver los decimales reales
std::cout << std::fixed << std::setprecision(20);
std::cout << "Suma de 0.1 + 0.2: " << suma << "\n";
std::cout << "¿Es igual a 0.3?: " << (suma == 0.3 ? "Sí" : "No") << "\n\n";
// 2. Comparación correcta usando un margen de error (Epsilon)
double objetivo = 0.3;
// std::numeric_limits<T>::epsilon() devuelve la diferencia más pequeña
// entre 1.0 y el siguiente valor representable.
double epsilon = std::numeric_limits<double>::epsilon();
// Para comparaciones entre números que no son 1.0, solemos usar un
// umbral razonable o un error relativo.
if (std::abs(suma - objetivo) < 1e-9) {
std::cout << "Comparación con tolerancia: Son iguales\n";
}
// 3. Valores especiales: Infinito y NaN
double inf = std::numeric_limits<double>::infinity();
double nan_val = std::numeric_limits<double>::quiet_NaN();
std::cout << "Infinito: " << inf << "\n";
// NaN (Not a Number) tiene la propiedad única de no ser igual a sí mismo
std::cout << "¿Es NaN? (usando std::isnan): " << (std::isnan(nan_val) ? "Sí" : "No") << "\n";
std::cout << "¿NaN == NaN?: " << (nan_val == nan_val ? "Sí" : "No") << "\n\n";
// 4. Acumulación de error y Fused Multiply-Add (FMA)
// Al sumar 0.1 diez veces, el error se propaga.
double acumulado = 0.0;
for (int i = 0; i < 10; ++i) {
acumulado += 0.1;
}
std::cout << "Suma de 0.1 diez veces: " << acumulado << "\n";
std::cout << "¿Es exactamente 1.0?: " << (acumulado == 1.0 ? "Sí" : "No") << "\n";
// std::fma(a, b, c) calcula (a * b) + c con un solo redondeo final,
// reduciendo el error de redondeo en operaciones combinadas.
double producto_suma = std::fma(0.1, 0.2, 0.3);
std::cout << "Resultado con std::fma(0.1, 0.2, 0.3): " << producto_suma << "\n";
return 0;
}
Análisis del código
En el bloque de suma inicial, a + b no resulta en exactamente 0.3. El compilador y la FPU (Unidad de Punto Flotante) almacenan la aproximación más cercana de 0.1 y 0.2. Al sumarlas, el bit menos significativo de la mantisa de la suma no coincide con la representación de 0.3, lo que hace que la comparación suma == 0.3 devuelva false.
Para resolver esto, aplicamos una tolerancia en la comparación std::abs(suma - objetivo) < 1e-9. En lugar de buscar la igualdad perfecta, verificamos si la diferencia es lo suficientemente pequeña como para considerarse despreciable.
En la sección de valores especiales, notarás que nan_val == nan_val es false. Esta es una decisión del estándar IEEE 754: NaN representa un valor indefinido (como 0.0 / 0.0), y dado que no tiene un valor real, no puede ser igual a nada, ni siquiera a sí mismo. Para detectarlo, es obligatorio usar std::isnan.
Finalmente, en el bucle de acumulación, el error de redondeo se suma en cada iteración. Si sumas un número con error mil veces, el error final puede ser lo suficientemente grande como para invalidar tu lógica. Para mitigar esto en cálculos complejos, se utiliza std::fma, que realiza la multiplicación y la suma en un solo paso de precisión extendida antes de redondear, minimizando la pérdida de información.
El error frecuente
El error más común es intentar usar == para controlar la lógica de un programa basado en decimales.
double saldo = 0.0;
double deposito = 0.1;
for(int i = 0; i < 10; ++i) saldo += deposito;
if (saldo == 1.0) { // ¡ERROR! Esto será falso
// Código que nunca se ejecuta
}
Este error es especialmente peligroso porque no genera un error de compilación ni un crash. El programa simplemente se comporta de forma incorrecta. Si usas herramientas como AddressSanitizer, no detectarás nada porque no hay errores de memoria, sino de lógica matemática. Siempre utiliza un margen de error (épsilon) o trabaja con enteros (por ejemplo, representando dinero en céntimos en lugar de euros) para evitar este problema.
Para cálculos científicos de alta precisión, utiliza std::fma para reducir el ruido de redondeo en operaciones compuestas.
N° 16