Можно ли сравнивать плавающие точки с 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 - это мертвый код, и компилятор просто удаляет его, если оптимизация включена.
Вы не указали формат с плавающей запятой и не были стандартными, если таковые были в интересующем вас формате. В некоторых форматах есть проблема с +/- нулевым значением, например, например.