JavaRush /Курсы /C++ SELF /NaN

NaN / Infinity — обратная сторона математики

C++ SELF
10 уровень , 2 лекция
Открыта

1. Зачем знать про NaN и Infinity

Когда вы только начинаете работать с double, кажется, что это «обычные числа, только с точкой». Но у вещественной арифметики есть особенность: некоторые вычисления не приводят к аварийному завершению, а дают специальные значения — и программа продолжает жить так, будто ничего не случилось. Это удобно для научных расчётов, но очень опасно для прикладного кода, где вы ждёте «если ошибка — остановись».

Три главных «особых гостя» сегодня:

  • Infinity (положительная бесконечность)
  • -Infinity (отрицательная бесконечность)
  • NaN (Not a Number, «не число»)

Они называются «тихими», потому что легко просачиваются в расчёты: вы делаете формулу, выводите результат, а там внезапно nan или inf. И это ещё полбеды. Хуже, когда NaN попадает в if, и ветвления начинают вести себя так, будто у программы пропало чувство реальности.

2. Infinity и -Infinity: откуда берутся

Если говорить по-человечески, Infinity появляется там, где вы пытаетесь получить «слишком большое число» или делите на ноль в вещественной арифметике. В математике «делить на ноль нельзя», а компьютер в режиме double часто отвечает: «ладно, пусть будет бесконечность», и идёт дальше. Это и есть типичный источник «тихих» проблем: ошибка произошла, но программа не остановилась.

Самый простой пример — деление double на ноль. Сразу важная оговорка: целочисленное деление на ноль — отдельная история (и обычно заканчивается плохо). Мы сейчас говорим именно про double.

#include <iostream>

int main() {
    double a = 1.0;
    double b = 0.0;

    double x = a / b;
    std::cout << x << '\n'; // inf (часто печатается как "inf")
}

Можно получить и отрицательную бесконечность:

#include <iostream>

int main() {
    double a = -1.0;
    double b = 0.0;

    std::cout << (a / b) << '\n'; // -inf
}

Есть и другой путь: некоторые вычисления «взрываются» по масштабу. Например, очень большие значения после множества умножений могут выйти за диапазон представления double. Тогда вместо «огромного числа» получится inf. Это поведение зависит от платформы и настроек, но как идея — полезно помнить: у double есть предел, и «слишком большое» превращается в бесконечность.

3. NaN: откуда берётся «не число»

NaN — это когда результат не просто «очень большой», а вообще не имеет смысла как вещественное число в рамках обычной арифметики. Представьте калькулятор, который вместо ответа пишет: «я в замешательстве». Это NaN.

Самый типичный источник NaN — операции вида 0.0 / 0.0 или «неопределённые» действия с бесконечностями (например, inf - inf). Вы можете даже не замечать, как до этого дошли: сначала вы вычислили какой-то знаменатель, он стал нулём из-за погрешности/логики, потом поделили — и вот уже в данных живёт NaN.

#include <iostream>

int main() {
    double zero = 0.0;
    double x = zero / zero;

    std::cout << x << '\n'; // nan (часто печатается как "nan")
}

Ещё классический пример:

#include <iostream>
#include <limits>

int main() {
    double inf = std::numeric_limits<double>::infinity();
    double x = inf - inf;

    std::cout << x << '\n'; // nan (часто)
}

Заметьте, что здесь мы использовали std::numeric_limits<double>::infinity() — это стандартный способ получить «настоящую» бесконечность. А сам факт, что стандартная библиотека отдельно обсуждает случаи NaN и inf даже на уровне форматирования, хорошо намекает: это не «экзотика», а нормальная часть модели вещественных чисел.

4. Почему NaN опасен: ломает сравнения и if

Вот тут начинается самое интересное (и слегка коварное). Infinity ещё можно воспринимать как «очень-очень большое число» (хотя это тоже не совсем число). А NaN — это значение, которое ведёт себя так, будто оно всем недовольно и ни с чем не соглашается.

Любое сравнение с NaN возвращает false.

Даже такое:

  • NaN == NaN -> false
  • NaN < 0.0 -> false
  • NaN > 0.0 -> false

В итоге условие if (x < 0) внезапно не срабатывает, хотя вы «видите», что x плохой. Просто потому, что x — не число, и сравнение не имеет смысла.

Давайте посмотрим на это в коде.

#include <iostream>
#include <limits>

int main() {
    double x = std::numeric_limits<double>::quiet_NaN();

    std::cout << (x == x) << '\n'; // 0 (false)
    std::cout << (x != x) << '\n'; // 1 (true)
}

Да, это выглядит как баг матрицы, но это специальное свойство NaN. И оно же даёт нам простой «детектор NaN на базовом уровне»: если x != x, то xNaN.

Чтобы закрепить, маленькая табличка поведения:

Выражение Результат
nan == nan
false
nan != nan
true
nan < 0.0
false
nan > 0.0
false
nan <= 0.0
false
nan >= 0.0
false

И вот почему это называют «тихой поломкой логики»: программа не падает, просто условие внезапно перестаёт работать так, как вы ожидаете.

5. Как получить NaN и Infinity через numeric_limits

В реальной программе вы обычно не хотите создавать NaN и Infinity. Вы хотите их обнаружить и принять решение: вывести ошибку, остановить расчёт, попросить пользователя ввести другие данные.

Но для диагностики полезно уметь получить эти значения «легальным способом» — чтобы тестировать и понимать поведение.

Для этого есть заголовок <limits> и шаблон std::numeric_limits<T>.

#include <iostream>
#include <limits>

int main() {
    double inf = std::numeric_limits<double>::infinity();
    double nan = std::numeric_limits<double>::quiet_NaN();

    std::cout << inf << '\n'; // inf
    std::cout << nan << '\n'; // nan
}

Почему это лучше, чем «сделать 1.0/0.0»? Потому что numeric_limits — это явный и читаемый способ: вы сразу видите в коде намерение. А 1.0/0.0 выглядит как ошибка, и ваш будущий вы (или ваш преподаватель) начнёт нервно чесать затылок.

6. Как NaN/Infinity расползаются по вычислениям

У NaN есть неприятная суперспособность: он редко остаётся в одном месте. Обычно он превращает в NaN всё, до чего дотянется. Условно: если у вас в формуле участвует NaN, то результат чаще всего тоже станет NaN. И через несколько шагов вы уже не понимаете, где именно он появился.

Это удобно показать «цепочкой»:

flowchart TD
    A["Ошибка/особый случай: деление на 0"] --> B["Получили NaN/Infinity"]
    B --> C["Сделали ещё вычисление"]
    C --> D["NaN/Infinity попали в новые переменные"]
    D --> E["if/цикл/условие начинает вести себя странно"]
    E --> F["Баг выглядит как 'логика сломалась сама'"]

Мини-пример «заражения»:

#include <iostream>
#include <limits>

int main() {
    double nan = std::numeric_limits<double>::quiet_NaN();
    double a = nan + 10.0;
    double b = a * 2.0;

    std::cout << a << '\n'; // nan
    std::cout << b << '\n'; // nan
}

Infinity тоже «расползается», но более предсказуемо: inf + 1 остаётся inf, inf * 2 остаётся inf, и только некоторые комбинации (например, inf - inf) дают NaN.

7. Защита в консольном приложении

Представим, что к этому моменту курса у нас есть простое консольное приложение «мини-калькулятор команд», которое читает команду и два числа. Раньше мы могли считать на int, а сегодня начинаем переходить к double.

Пусть команды такие: add, sub, mul, div. Здесь div как раз самое опасное: если знаменатель 0, можем получить inf или nan и тихо «поехать дальше».

Сделаем минимальный кусочек: считаем result, а затем проверяем на «особость». Пока без продвинутых функций вроде std::isnan (их можно встретить в <cmath>, но мы держимся ближе к базовой модели), используем то, что уже знаем из лекции: сравнение с бесконечностью и трюк x != x.

#include <iostream>
#include <limits>
#include <string>

int main() {
    std::string op;
    double a = 0.0, b = 0.0;
    std::cin >> op >> a >> b;

    double result = (op == "div") ? (a / b) : 0.0;

    const double inf = std::numeric_limits<double>::infinity();
    if (result != result) 
      std::cout << "Error: NaN\n";
    else if (result == inf || result == -inf) 
      std::cout << "Error: Infinity\n";
    else 
      std::cout << result << '\n';
}

Что здесь важно.

Сначала мы ввели inf через numeric_limits, чтобы не писать «магические» вычисления. Потом мы проверили result != result — это ловит NaN. И затем мы сравнили с inf и -inf, чтобы поймать бесконечность. Если всё нормально — печатаем результат.

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

Демонстрация «тихого бага» без проверок

Чтобы почувствовать боль (чуть-чуть, в учебных целях), сравним с наивным вариантом: «посчитали и сразу используем в условии».

#include <iostream>

int main() {
    double x = 0.0 / 0.0; // nan

    if (x < 0.0) {
        std::cout << "negative\n";
    } else {
        std::cout << "not negative\n"; // not negative (хотя x вообще не число)
    }
}

Если вы ожидали, что «плохое значение» попадёт в ветку negative или вызовет ошибку — нет. Оно просто делает сравнение бессмысленным, и условие даёт false.

8. Полезные нюансы

Infinity в логике: не просто «очень большое»

Легко попасть в ловушку: воспринимать inf как «ну это почти 1e9999». Но у inf есть поведение, которое ломает «интуитивные» проверки диапазонов.

Например, вы можете сделать ограничение: «если результат больше 1e6 — это слишком много». С inf это сработает (inf > 1e6 даст true), и кажется: «всё нормально». Но если вы дальше начнёте делать обратные операции, то может получиться не то, что вы ждёте.

Как inf превращается в 0.0 и почему это опасно

Иногда люди пытаются «нормализовать» число, делая 1.0 / x. Если x вдруг inf, то 1.0 / inf превращается в 0.0. И вы из «катастрофически большого» значения внезапно получаете «аккуратный ноль», который потом участвует в делении, и дальше по цепочке уже появляется NaN. Такие цепочки неприятны именно тем, что каждый шаг выглядит законно.

Мини-пример:

#include <iostream>
#include <limits>

int main() {
    double inf = std::numeric_limits<double>::infinity();
    double x = 1.0 / inf;

    std::cout << x << '\n'; // 0
}

Ноль получился «честно», но если ваша логика не ожидала нулей (или деления на ноль), дальше будет весело.

9. Типичные ошибки при работе с NaN и Infinity

Ошибка №1: пытаться проверить NaN через x == std::numeric_limits<double>::quiet_NaN().
Это выглядит логично: «сравню с NaN и узнаю». Но NaN специально устроен так, что NaN == NaN даёт false. Поэтому такая проверка никогда не сработает. На базовом уровне проще запомнить трюк x != x, а в более библиотечном стиле (к которому мы ещё придём) используются специальные функции проверки.

Ошибка №2: думать, что деление на ноль всегда аварийно останавливает программу.
В целых числах часто действительно всё плохо, а вот в double вы можете получить inf или nan, и программа продолжит выполняться. Это опасно тем, что ошибка «не кричит», а тихо портит данные.

Ошибка №3: использовать NaN в условиях как обычное число.
Люди пишут if (x < 0.0), if (x >= limit), if (x == 0.0) — и ожидают, что «плохое значение» куда-то попадёт. Но NaN почти всегда заставляет сравнение вернуть false. Поэтому ветвления начинают идти «по дефолту», и создаётся ощущение, что if сломан, хотя на самом деле сломано значение.

Ошибка №4: не проверять результаты вычислений на «особые значения» в местах, где есть риск.
Если в формуле есть деление, корень, логарифм или вычисления на границе допустимого диапазона, то проверка результата (или входных данных) становится частью нормальной гигиены кода. Особенно в учебных мини-программах это помогает ловить ошибки раньше: лучше честно вывести Error: Infinity, чем получить nan и потом искать его происхождение по всей программе.

Ошибка №5: путать «красивый вывод» с «корректными данными».
Иногда std::cout печатает nan/inf явно, а иногда (в зависимости от настроек форматирования) вывод может выглядеть не так заметно. В любом случае форматирование — это только внешний вид. Проверять корректность нужно по значениям, а не по тому, «симпатично ли напечаталось» в консоли.

1
Задача
C++ SELF, 10 уровень, 2 лекция
Недоступна
Парадокс NaN
Парадокс NaN
1
Задача
C++ SELF, 10 уровень, 2 лекция
Недоступна
Тип деления
Тип деления
1
Задача
C++ SELF, 10 уровень, 2 лекция
Недоступна
Консольный калькулятор
Консольный калькулятор
1
Задача
C++ SELF, 10 уровень, 2 лекция
Недоступна
Цепочка операций
Цепочка операций
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ