Сравнение float и double в C++: как избежать ошибок из-за потери точности?
Проблема прямого сравнения
Числа с плавающей точкой (float, double) представляются в двоичном виде с ограниченной мантиссой. Из-за этого многие десятичные дроби (например, 0.1) не могут быть представлены точно. Ошибки накапливаются при арифметических операциях, поэтому прямое сравнение через == почти всегда приводит к ложным результатам.
Решение: сравнение с погрешностью (epsilon)
Основной подход — сравнивать не сами числа, а модуль их разности с некоторым малым значением (epsilon). Если |a - b| < epsilon, считаем числа равными. Главная сложность — правильно выбрать epsilon.
Абсолютная погрешность (простейший случай)
Если числа находятся в небольшом диапазоне (например, от -1 до 1), можно использовать фиксированное значение:
#include <cmath>
#include <iostream>
bool almostEqual(float a, float b, float epsilon = 1e-6f) {
return std::fabs(a - b) < epsilon;
}
int main() {
float x = 0.1f + 0.2f;
float y = 0.3f;
std::cout << std::boolalpha << almostEqual(x, y); // true
}
Но этот метод ломается для очень больших чисел (разница в 1e-6 ничтожна для 1e9) и для очень маленьких (разница в 1e-6 может быть больше самих чисел).
Относительная погрешность (универсальный метод)
Лучше сравнивать с учетом масштаба чисел. Используем std::numeric_limits для получения машинного epsilon — разницы между 1.0 и следующим представимым числом:
#include <cmath>
#include <limits>
#include <iostream>
template<typename T>
bool almostEqualRelative(T a, T b, int maxUlps = 4) {
// Разница по абсолютной величине
T diff = std::fabs(a - b);
// Если числа очень близки к нулю, используем абсолютную погрешность
if (diff < std::numeric_limits<T>::min())
return true;
// Относительная погрешность: нормируем на максимальное из чисел
T largest = std::max(std::fabs(a), std::fabs(b));
return diff <= largest * std::numeric_limits<T>::epsilon() * maxUlps;
}
int main() {
double a = 1.0e10;
double b = a + 1.0; // разница в 1.0, но числа огромны
std::cout << almostEqualRelative(a, b); // false (правильно)
double c = 1.0e-10;
double d = c * 1.0000001; // очень маленькое отличие
std::cout << almostEqualRelative(c, d); // true
}
Параметр maxUlps (Units in the Last Place) задаёт, сколько последних бит мантиссы мы разрешаем отличаться. Для float обычно хватает 4, для double — 4-8.
Специализированная функция для float vs double
Если нужно сравнивать float и double, приведите их к одному типу (лучше к double, так как он точнее):
bool compareFloatDouble(float a, double b) {
return almostEqualRelative<double>(static_cast<double>(a), b);
}
Практические рекомендации
- Для физических симуляций используйте
doubleи относительную погрешность сmaxUlps = 4. - Для финансовых расчётов никогда не используйте float/double — берите десятичные типы или целые числа (копейки).
- Для проверки на равенство нулю используйте абсолютную погрешность:
std::fabs(x) < 1e-12. - Избегайте сравнений в циклах с плавающей точкой. Лучше использовать целые счётчики.
Готовое решение (C++17+)
#include <cmath>
#include <limits>
#include <type_traits>
template<typename T, typename U>
bool safeEquals(T a, U b) {
static_assert(std::is_floating_point_v<T> && std::is_floating_point_v<U>,
"Only for floating point types");
using Common = std::common_type_t<T, U>;
Common ca = static_cast<Common>(a);
Common cb = static_cast<Common>(b);
Common diff = std::fabs(ca - cb);
if (diff < std::numeric_limits<Common>::min())
return true;
Common largest = std::max(std::fabs(ca), std::fabs(cb));
return diff <= largest * std::numeric_limits<Common>::epsilon() * 4;
}
Этот шаблон работает для любых комбинаций float/double/long double и автоматически выбирает подходящую точность.
<<Альтернативный подход: сравнение через ULP (Units in the Last Place)
Более точный, но менее интуитивный метод — сравнивать целочисленные представления чисел с плавающей точкой. В IEEE 754 числа одинакового знака можно сравнивать как целые числа после приведения битовой маски.
#include <cstring> // для memcpy (избегаем strict aliasing)
#include <iostream>
bool almostEqualUlp(float a, float b, int maxUlps = 4) {
int32_t ai, bi;
std::memcpy(&ai, &a, sizeof(a));
std::memcpy(&bi, &b, sizeof(b));
// Обрабатываем отрицательные числа (их порядок обратный)
if (ai < 0) ai = 0x80000000 - ai;
if (bi < 0) bi = 0x80000000 - bi;
int32_t diff = std::abs(ai - bi);
return diff <= maxUlps;
}
Преимущества:
- Работает для всего диапазона чисел (от денормализованных до огромных).
- Не требует вычислений с плавающей точкой (быстрее).
- Интуитивно понятен: "сколько бит точности мы готовы потерять".
Недостатки:
- Сложнее для понимания новичками.
- Требует аккуратной работы с битовыми представлениями.
- Не работает напрямую для сравнения float с double (нужно приводить к одному типу).
Этот метод особенно полезен в высокопроизводительных вычислениях или когда нужно строго контролировать погрешность на уровне битов. Для большинства повседневных задач достаточно относительной погрешности из первого ответа.
Ответов пока нет.
Войдите чтобы оставить ответ