Подтвердить что ты не робот

Как проверить преобразование двойного/целочисленного без потерь?

У меня есть один двойной и один 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.
4b9b3361

Ответ 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);
}