5.7 KiB
Числа с плавающей точкой
С float и double в принципе всегда все сложно. Особенно в C++.
Стандарт C++ не требует следования стандарту IEEE 754, потому деление на ноль в вещественных числах также считается неопределенным поведением, несмотря на то, что
по IEEE 754 выражение x/0.0 определяется как -INF, NaN, или INF в зависимости от знака числа x (NaN для нуля).
Сравнение вещественных чисел — излюбленная головная боль.
Выражение x == y фактически является кривым побитовым сравнением для чисел с плавающей точкой, по особенному работающее со случаями -0.0 и +0.0, и NaN.
О существовании этого и != операторов для вещественных чисел стоит забыть и никогда не вспоминать.
Для побитового сравнения нужно использовать memcmp.
Для сравнения чисел — приближенные варианты вида std::abs(x - y) < EPS, где EPS — какое-то абсолютное или вычисляемое на основе x и y значение. А также различные манипуляции с ULP сравниваемых чисел.
Так как стандарт C++ не форсирует IEEE 754,
проверки на x == NaN через его свойство (x != x) == true могут быть убраны компилятором, как заведомо ложные. Проверять нужно с помощью предназначенных для этого
функций std::isnan.
Поддерживается или нет IEEE 754 можно проверить с помощью предопределенной константы
std::numeric_limits<FloatType>::is_iec559
Сужающие преобразования из float в знаковые или беззнаковые целые могут повлечь неопределенное поведение, если значение непредставимо в целочисленном типе. Никаких обрезок по модулю 2^N не предполагается.
constexpr uint16_t x = 1234567.0; // CE, undefined behavior
Обратное преобразование, из целочисленных типов во float/double, также имеет свои подвохи, не связанные с неопределенным поведением: большие по абсолютной величине целые числа теряют точность
static_assert(
static_cast<float>(std::numeric_limits<int>::max()) ==
static_cast<float>(static_cast<long long>(std::numeric_limits<int>::max()) + 1) // OK
);
static_assert(
static_cast<double>((1LL << 53) - 1) ==
static_cast<double>(1LL << 53) // fire!
);
static_assert(
static_cast<double>((1LL << 54) - 1) ==
static_cast<double>(1LL << 54) // OK
);
static_assert(
static_cast<double>((1LL << 55) - 1) ==
static_cast<double>(1LL << 55) // OK
);
static_assert(
static_cast<double>((1LL << 56) - 1) ==
static_cast<double>(1LL << 56) // OK
);
В качестве домашнего задания читателю предлагается самостоятельно сформулировать, почему никогда нельзя хранить деньги в типах с плавающей запятой.
Плавающая точка и шаблоны
Вещественные числа до C++20 нельзя было использовать в качестве параметров-значений в шаблонах. Теперь же можно. Правда, ожидать, что вы насчитаете в run-time и в compile-time одно и то же — не стоит.
Для простой параметризации типов константами этот механизм вполне можно использовать без опасений. Однако строить на них паттерн-матчинг с выбором специализаций шаблонов крайне не рекомендуется:
template <double x>
struct X {
static constexpr double val = x;
};
template <>
struct X<+0.> {
static constexpr double val = 1.0;
};
template <>
struct X<-0.> {
static constexpr double val = -1.0;
};
int main() {
constexpr double a = -3.0;
constexpr double b = 3.0;
std::cout << X<a + b>::val << "\n"; // печатает +1
std::cout << X<-1.0 * (a + b)>::val << "\n"; // печатает -1
static_assert(a + b == -1.0 * (a + b)); // ok
}
По тем же причинам ни в одном языке программирования не рекомендуется использовать значения с плавающей точкой в качестве ключей ассоциативных массивов.