С 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
}
По тем же причинам ни в одном языке программирования не рекомендуется использовать значения с плавающей точкой в качестве ключей ассоциативных массивов.