Можно ли сравнивать плавающие точки с 0.0 без epsilon?

Я знаю, что для сравнения двух значений с плавающей запятой нужно использовать некоторую точность epsilon, поскольку они не точны. Тем не менее, мне интересно, есть ли случаи с краем, где мне не нужен этот epsilon.

В частности, я хотел бы знать, всегда ли безопасно делать что-то вроде этого:

double foo(double x){
    if (x < 0.0) return 0.0;
    else return somethingelse(x); // somethingelse(x) != 0.0
}

int main(){
   int x = -3.0;
   if (foo(x) == 0.0) { 
     std::cout << "^- is this comparison ok?" << std::endl; 
   }
}

Я знаю, что есть лучшие способы написания foo (например, возврат флага дополнительно), но мне интересно, можно ли вообще назначить 0.0 переменной с плавающей запятой, а затем сравнить ее с 0.0.

Или более общее, всегда ли всегда выполняется сравнение?

double x = 3.3;
double y = 3.3;
if (x == y) { std::cout << "is an epsilon required here?" << std::endl; }

Когда я это пробовал, он, похоже, работает, но, возможно, не следует полагаться на это.

Ответ 1

Да, если вы вернетесь 0.0, вы можете сравнить его с 0.0; 0 представляется точно как значение с плавающей запятой. Если вы вернетесь 3.3, вы должны быть гораздо более осторожны, так как 3.3 не является точно представимым, поэтому, например, преобразование из double в float приведет к другому значению.

Ответ 2

Да, в этом примере отлично проверить == 0.0. Это связано не только с тем, что 0.0 является особенным, а потому, что вы только присваиваете значение и сравниваете его впоследствии. Вы также можете установить его на 3.3 и сравнить для == 3.3, это тоже будет хорошо. Вы храните битовый шаблон и сравниваете его для одного и того же битового шаблона, если для сравнения не присваиваются значения другому типу.

Однако результаты вычислений, которые математически равны нулю, не всегда будут равны 0.0.


Этот Q/A развился так же, как и случаи, когда разные части программы компилируются разными компиляторами. В вопросе этого не упоминается, мой ответ применяется только тогда, когда один и тот же компилятор используется для всех соответствующих частей.

С++ 11 Standard,
§5.10 Операторы равенства

6 Если оба операнда имеют тип арифметики или перечисления, обычный арифметические преобразования выполняются на обоих операндах; каждый из операторы должны дать true, если указанная связь истинна и false, если оно ложно.

Отношения не определены далее, поэтому мы должны использовать общее значение "equal".

§2.13.4 Плавающие литералы

1 [...] Если масштабированное значение находится в диапазоне отображаемых значений для его типа результатом является масштабированное значение, если оно представлено, иначе большее или меньшее представляемое значение, ближайшее к масштабированному значению, выбранных в соответствии с реализацией. [...]

Компилятор должен выбирать между двумя значениями при преобразовании литерала, когда значение не представляется. Если одно и то же значение выбирается для одного и того же литерала последовательно, вы можете сравнивать значения, такие как 3.3, потому что == означает "равно".

Ответ 3

: 0, поскольку значение с плавающей запятой не является уникальным, но IEEE 754 определяет сравнение 0.0==-0.0 как истинного (любой нуль в этом отношении).

Итак, с 0.0 это работает - для каждого другого номера это не так. Литерал 3.3 в одном блоке компиляции (например, в библиотеке), а другой (например, ваше приложение) может отличаться. Стандарт требует, чтобы компилятор использовал одно и то же округление, которое он использовал бы во время выполнения, но разные параметры компилятора/компилятора могли бы использовать различное округление.

Он будет работать большую часть времени (для 0), но это очень плохая практика.

Пока вы используете один и тот же компилятор с одинаковыми настройками (например, один блок компиляции), он будет работать, потому что литерал 0.0 или 0.0f будет каждый раз переводить на один и тот же бит. Однако представление нуля не является уникальным. Поэтому, если foo объявлено в библиотеке и ваш вызов к нему в каком-либо приложении, то одна и та же функция может выйти из строя.

Вы можете спасти этот самый случай, используя std::fpclassify, чтобы проверить, представляет ли возвращаемое значение ноль. Для каждого конечного (ненулевого) значения вам придется использовать epsilon-сравнение, хотя, если вы не останетесь в одном модуле компиляции и не выполняете никаких операций над значениями.

Ответ 4

Как написано в обоих случаях, вы используете одинаковые константы в том же файле, который подается в один и тот же компилятор. Преобразование строки в float, используемое компилятором, должно возвращать один и тот же шаблон битов, чтобы они не только были равны, как в случае с плюсом или минусом для нулевой вещи, но понемногу равны.

Если у вас есть константа, которая использует библиотеку C для операционных систем для генерации битового шаблона, тогда есть строка для f или что-то, что может использовать другую библиотеку C, если двоичный файл переносится на другой компьютер, чем тот, который скомпилирован, У вас может быть проблема.

Разумеется, если вы вычислите 3.3 для одного из терминов, времени выполнения и еще раз вычислите время компиляции 3.3, вы можете и получите сбои при равных сравнениях. Очевидно, что некоторые константы с большей вероятностью работают, чем другие.

Конечно, как написано, ваше сравнение 3.3 - это мертвый код, и компилятор просто удаляет его, если оптимизация включена.

Вы не указали формат с плавающей запятой и не были стандартными, если таковые были в интересующем вас формате. В некоторых форматах есть проблема с +/- нулевым значением, например, например.