Как проверить преобразование двойного/целочисленного без потерь?
У меня есть один двойной и один int64_t. Я хочу знать, имеют ли они одно и то же значение, и если преобразование одного типа в другое не потеряет никакой информации.
Моя текущая реализация такова:
int int64EqualsDouble(int64_t i, double d) {
return (d >= INT64_MIN)
&& (d < INT64_MAX)
&& (round(d) == d)
&& (i == (int64_t)d);
}
Мой вопрос: правильно ли эта реализация? А если нет, то что будет правильным ответом? Чтобы быть верным, он не должен оставлять ложных срабатываний и не иметь ложных отрицательных результатов.
Некоторые примеры ввода:
- int64EqualsDouble (0, 0.0) должен возвращать 1
- int64EqualsDouble (1, 1.0) должен возвращать 1
- int64EqualsDouble (0x3FFFFFFFFFFFFFFF, (double) 0x3FFFFFFFFFFFFFFF) должен возвращать 0, потому что 2 ^ 62 - 1 может быть точно представлен с int64_t, но не с двойным.
- int64EqualsDouble (0x4000000000000000, (double) 0x4000000000000000) должен возвращать 1, потому что 2 ^ 62 может быть точно представлен как в int64_t, так и в double.
- int64EqualsDouble (INT64_MAX, (double) INT64_MAX) должен возвращать 0, поскольку INT64_MAX не может быть точно представлен как двойной
- int64EqualsDouble (..., 1.0e100) должен возвращать 0, потому что 1.0e100 не может быть точно представлен как int64_t.
Ответы
Ответ 1
Да, ваше решение работает правильно, потому что оно было создано для этого, потому что int64_t
представляется в двух дополнениях по определению (C99 7.18.1.1:1) на платформах, которые используют нечто похожее на двоичную IEEE 754 с двойной точностью для тип double
. Это в основном то же самое, что этот.
В этих условиях:
-
d < INT64_MAX
правильный, поскольку он эквивалентен d < (double) INT64_MAX
, а при преобразовании в double число INT64_MAX
, равное 0x7fffffffffffffff, округляется. Таким образом, вы хотите, чтобы d
был строго меньше результирующего double
, чтобы избежать запуска UB при выполнении (int64_t)d
.
-
С другой стороны, INT64_MIN
, будучи -0x8000000000000000, является точно представимым, что означает, что double
, равное (double)INT64_MIN
, может быть равно некоторому int64_t
и не должно быть исключено ( и такой double
может быть преобразован в int64_t
без запуска поведения undefined)
Само собой разумеется, что, поскольку мы специально использовали предположения о 2 дополнениях для целых чисел и двоичной с плавающей запятой, правильность кода не гарантируется этим рассуждением на разных платформах. Возьмите платформу с двоичным 64-битным числом с плавающей запятой и 64-разрядным целым целым числом T
. На этой платформе T_MIN
есть -0x7fffffffffffffff
. Преобразование в double
этого числа округляется, что приводит к -0x1.0p63
. На этой платформе, используя вашу программу, как она написана, использование -0x1.0p63
для d
делает первые три условия истинными, что приводит к поведению undefined в (T)d
, потому что переполнение в преобразовании из целого числа в плавающую точку - это поведение undefined.
Если у вас есть доступ к полным функциям IEEE 754, существует более короткое решение:
#include <fenv.h>
…
#pragma STDC FENV_ACCESS ON
feclearexcept(FE_INEXACT), f == i && !fetestexcept(FE_INEXACT)
В этом решении используется преобразование из целочисленного значения в значение с плавающей точкой, устанавливающее флаг INEXACT, если преобразование неточно (то есть, если i
не представляется точно таким же, как double
).
Флаг INEXACT остается неустановленным, а f
равен (double)i
тогда и только тогда, когда f
и i
представляют одно и то же математическое значение в своих соответствующих типах.
Этот подход требует, чтобы компилятор был предупрежден о том, что код обращается к состоянию FPU, обычно с #pragma STDC FENV_ACCESS on
, но обычно не поддерживается, и вместо этого вы должны использовать флаг компиляции.
Ответ 2
В коде OP есть зависимость, которой можно избежать.
Для успешного сравнения d
должно быть целым числом, и round(d) == d
позаботится об этом. Даже d
, как NaN, не получится.
d
должен быть математически в диапазоне [ INT64_MIN
... INT64_MAX
], и если условия if
должным образом гарантируют, что окончательный i == (int64_t)d
завершает тест.
Итак, вопрос сводится к сравнению пределов INT64
с double
d
.
Предположим FLT_RADIX == 2
, но не обязательно IEEE 754 binary64.
d >= INT64_MIN
не является проблемой, так как -INT64_MIN
является степенью 2 и точно преобразуется в double
того же значения, поэтому >=
является точным.
Код хотел бы сделать математическое d <= INT64_MAX
, но это может не сработать и поэтому проблема. INT64_MAX
является "мощностью 2 - 1" и может не преобразовываться точно - это зависит от того, превышает ли точность double
63 бита, что делает сравнение неясным. Решение состоит в том, чтобы сократить вдвое сравнение. d/2
не претерпевает потери точности, а INT64_MAX/2 + 1
точно преобразуется в double
power-of-2
d/2 < (INT64_MAX/2 + 1)
[изменить]
// or simply
d < ((double)(INT64_MAX/2 + 1))*2
Таким образом, если код не хочет полагаться на double
с меньшей точностью, чем uint64_t
. (Что-то, что, вероятно, относится к long double
), более портативное решение будет
int int64EqualsDouble(int64_t i, double d) {
return (d >= INT64_MIN)
&& (d < ((double)(INT64_MAX/2 + 1))*2) // (d/2 < (INT64_MAX/2 + 1))
&& (round(d) == d)
&& (i == (int64_t)d);
}
Примечание. Отсутствуют проблемы с округлением.
[Изменить] Более подробное описание ограничения
Математически застраховать INT64_MIN <= d <= INT64_MAX
можно переформулировать как INT64_MIN <= d < (INT64_MAX + 1)
, поскольку мы имеем дело со целыми числами. Поскольку необработанное приложение (double) (INT64_MAX + 1)
в коде равно 0, альтернативой является ((double)(INT64_MAX/2 + 1))*2
. Это можно расширить для редких машин с double
с более высокими степенями от -2 до ((double)(INT64_MAX/FLT_RADIX + 1))*FLT_RADIX
. Лимиты сравнения, являющиеся точной степенью-2, преобразование в double
не претерпевает потери точности, а (lo_limit >= d) && (d < hi_limit)
является точным, независимо от точности с плавающей запятой. Обратите внимание: что редкая плавающая точка с FLT_RADIX == 10
по-прежнему является проблемой.
Ответ 3
В дополнение к подробному ответу Паскаля Куока и учитывая дополнительный контекст, который вы даете в комментариях, я бы добавил тест на отрицательные нули. Вы должны сохранить отрицательные нули, если у вас нет веских причин. Вам нужно специальное тестирование, чтобы избежать преобразования их в (int64_t)0
. С вашим текущим предложением отрицательные нули пройдут ваш тест, запишутся как int64_t
и вернутся в качестве положительных нулей.
Я не уверен, что самый эффективный способ проверить их, может быть, это:
int int64EqualsDouble(int64_t i, double d) {
return (d >= INT64_MIN)
&& (d < INT64_MAX)
&& (round(d) == d)
&& (i == (int64_t)d
&& (!signbit(d) || d != 0.0);
}