Фатализм в обработке ошибок -2


Предисловие


Эта статья является реакцией на статью: Что будет с обработкой ошибок в С++2a. После каждого абзаца у меня появлялся зуд, открывались зарубцованные раны и начинали кровоточить. Может, я принимаю слишком близко к сердцу то, что написано. Но просто хочется выть о той близорукости и безграмотности, что проявляют программисты на С++ в 21 веке. Причем даже не в его начале.


Приступим.


Классификация


Условно все ошибочные ситуации в программе можно разделить на 2 большие группы:
  1. Фатальные ошибки.
  2. Не фатальные, или ожидаемые ошибки.


Я сейчас буду придираться. Но фатальные ошибки — они тоже в каком-то смысле ожидаемые. Мы ожидаем, что проезд по памяти часто приводит к падению, но может к нему и не приводить. И это — ожидаемо, не правда ли? Когда вводится классификация, то всегда было бы проверить ее на непротиворечивость.


Но это так, частая малозаметная ошибка.


Давайте разберем фатальные ошибки.


Деление на 0. Интересно, почему эта ошибка является фатальной? Я бы с удовольствием кидал исключение в этом случае и ловил бы ее для последующей обработки. Почему она фатальная? Почему мне навязывается определенное поведение моей собственной программы, и я не могу никак на это повлиять? Разве С++ не про гибкость и про то, что язык повернут лицом к программисту? Хотя...


Разыменование нулевого указателя. Сразу вспоминается Java, там есть NullPointerException, который можно обработать. В библиотеке Poco есть тоже NullPointerException! Так почему разработчики стандарта с упорством глухонемого повторяют одну и ту же мантру?


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


В этом аспекте все споры про деление на ноль и разыменование указателей выглядят как борьба птичек за крошку хлеба. Безусловно, важный процесс. Но только с точки зрения птичек.


Вернемся к разделению на фатализм и его отсутствие… Начну с простого вопроса: если я получил неправильные данные по сети, является ли это фатальной ошибкой?


Простой и правильный ответ: зависит от. Понятно, что в большинстве случаев это не является фатальной ошибкой, и все данные, полученные по сети, надо провалидировать, и вернуть 4xx в случае ошибочности данных. А бывают ли случаи, когда надо крешнуться? Причем крешнуться с диким воем, чтобы пришла смс, например. Да еще и не одна.


Бывают. Могу привести пример из своей предметной области: распределенный алгоритм консенсуса. Нода получает ответ, который содержит хеш от цепочек изменений с другой ноды. И этот хеш отличается от локального. Это означает, что что-то пошло не так, и продолжать дальнейшее исполнение просто опасно: могут разойтись данные, если уже не. Бывает, когда доступность сервиса менее важна, нежели его консистентность. В этом случае нам нужно упасть, причем с грохотом, чтобы все услышали вокруг. Т.е. мы получили данные по сети, их провалидировали, и упали. Для нас эта ошибка — фатальнее некуда. Ожидаема ли эта ошибка? Ну да, мы же код написали с валидацией. Глупо утверждать обратное. Только мы не хотим продолжать выполнение программы после этого. Требуется ручное вмешательство, автоматика не сработала.


Выбор фатализма


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


Однако это — лишь видимая часть айсберга. В глубине стоит куда более чудовищный вопрос. Чтобы понять всю глубину глубин, надо поставить простую задачу и попытаться на нее ответить.


Задача. Сделать фреймворк чего-нибудь.


Все просто. Делаем фреймворк, например, сетевого взаимодействия. Или парсинга JSON. Или, на худой конец, XML. Сразу возникает вопрос: а вот когда возникает ошибка из сокета — это фатальная ошибка или нет? Перефразирую: надо ли кидать исключение, или вернуть ошибку? Это исключительная ситуация или нет? А может вернуть std::optional? Или монадку? (^1)


Парадоксальный вывод состоит в том, что сам фреймворк не может ответить на этот вопрос. Только использующий его код знает. Именно поэтому в превосходной, на мой взгляд, библиотеке boost.asio используется оба варианта. В зависимости от личных предпочтений автора кода и прикладной логики можно выбрать тот или иной способ обработки ошибок. Сначала меня немного смущал такой подход, но со временем я проникся гибкостью такого подхода.


Однако это еще не все. Самое страшное впереди. Вот мы пишем прикладной код, однако нам кажется, что он прикладной. Для другого кода, более высокоуровневого, наш код будет библиотечный. Т.е. разделение на прикладной/библиотечный(фреймворковый и т.п.) код — это чистая условность, которая зависит от уровня переиспользования компонент. Всегда можно что-то навертить сверху и прикладной код перестанет быть таковым. А это сразу означает, что выбор того, что является допустимым, а что — нет, уже решает код использующий, а не использованный.


Если же мы отпрыгнем в сторону, то окажется, что иногда даже нельзя понять, кто кого использует. Т.е. компонент А может использовать компонент Б, а компонент Б компонент А (^2). Т.е. кто определяет, что будет происходить, вообще непонятно.


Распутывание клубка


Когда смотришь на это все безобразие, то сразу возникает вопрос: как с этим жить? Что делать? Какие ориентиры для себя выбрать, чтобы не потонуть в многообразии?


Для этого полезно посмотреть по сторонам и понять, как такие вопросы решаются в других местах. Однако искать надо с умом: надо отличать "коллекционирование марок" от полноценных решений.


Что такое "коллекционирование марок"? Это собирательный термин, который означает, что мы разменяли цель но что-то другое. Например: была у нас цель — звонить и общаться с близкими людьми. И мы раз, и купили дорогущую игрушку, потому что "модно" и "красиво" (^3). Знакомо? Думаете, с программистами так не бывает? Не льстите себе.


Обработка ошибок — это не цель. Всякий раз, когда мы говорим про обработку ошибок, мы сразу приходим в тупик. Потому что это — способ достижения цели. А исходная цель — сделать наш софт надежным, простым и понятным. Именно такие цели надо ставить и всегда их придерживаться. А обработка ошибок — это фуфел, который не стоит обсуждения. Хочется кинуть исключение — да на здоровье! Вернул ошибку — молодец! Хочется монадку? Поздравляю, ты создал иллюзию продвинутости, но только в собственной башке (^4).


Тут хотел еще написать, как правильно делать, но уже исписался. Раны залечились и перестали кровоточить. Короче, советы такие:


  1. Разделяйте на компоненты с четкими границами.
  2. На границах описывайте, что и как может полететь. Желательно, чтобы было единообразно. Но гораздо важнее, чтобы было.
  3. Делайте возможность простой обработки ошибок в коде, который это будет использовать.
  4. Если что-то можно обработать внутри без нагрузки на пользовательский код — не выпячивайте это наружу. Чем меньше ошибок пользователь должен обрабатывать — тем лучше.
  5. Уважайте своего пользователя, не будьте мудаками! Пишите понятные интерфейсы с ожидаемым поведением, чтобы ему не нужно было читать комментарии и материться.

5-й совет самый главный, т.к. он объединяет первые четыре.


P.S. В детстве мне всегда любопытно было смотреть на муравейник. Тысячи муравьев, каждый что-то делает, ползет по своим делам. Процесс идет. Сейчас я тоже наблюдаю с интересом. Тоже за муравейником. Где тысячи особей занимаются своим маленьким делом. Могу пожелать им удачи в их нелегком деле!


^1: Люди падки на модные штуки. Когда все вдоволь наигрались, проснулись С++ программисты, и тут все завертелось.


^2: Такое может быть, когда есть несколько абстракций в компоненте В, которая их связывает. См. Инверсия управления.


^3: А на следующий день, бац, и экран разбился.


^4: Я не против монад, я против того, чтобы относиться к этому с придыханием, типа, смотрите, здесь монада, т.е. моноид в моноидальной категории эндофункторов! Слышны аплодисменты и одобрительные кивки. А где-то далеко-далеко, еле слышно, кто-то оргазмирует.

Вы можете помочь и перевести немного средств на развитие сайта



Комментарии (39):

  1. qthree
    /#19253909

    Фатальная ошибка = баг в программе. Деление на ноль, выход за предел массива, stack overflow — это все баги, которые не должны встречаться в корректной программе. Соответственно, если такая ошибка все же обнаружилась, то логичнее всего следует остановить программу и сообщить разработчику о баге.
    То что в продакшене может встречаться фатальная ошибка — это уже отдельный разговор.

    • gridem
      /#19253917

      Не все баги являются фатальными. Почему-то этот простой и понятный тезис может быть оспорен. На мой взгляд, это очевидно. Поэтому деление на ноль — это:


      1. Может быть не багом, а специальным допущением, который можно корректно обработать.
      2. Может быть багом, после которого программа может откатиться к некоторому валидному состоянию и продолжить нормальное исполнение далее.

      • qthree
        /#19254251

        Если деление на ноль не предусмотренно в конкретной операции и для этого не написано специальной логики, которая эту ситуацию обработывает — то да, это баг. Т.е. программа имеет в своем коде ошибку, которую надо исправить для дальнейшей корректной работы.

        • gridem
          /#19254285

          А если предусмотрено? То значит, что деление на ноль не фатально?

          • S-trace
            /#19258041

            А если в конкретном месте кода деление на ноль предусмотрено — то там код вида


            if (divisor == 0) {
                throw new ArithmeticException();
            }

            который делает деление на ноль не фатальным.

            • gridem
              /#19260163

              throw new ...


              А вы, часом, не джавист?


              И да, я хочу, чтобы такое мне делал компилятор. А ручками я могу и на асме пописать, если захочу.

      • kosmos89
        /#19258629

        Ну деление на ноль приводит к ошибке потому что в Си оно приводило к ошибке. А в Си оно приводило к ошибке потому что большинство процессоров валятся с ошибкой при попытке делить на ноль. А делать обвязку, замедляющую выполнение, для обхода этого поведения — совсем не в стиле Си/Си++.

        • RPG18
          /#19260021

          Как раз в стиле Си/Си++. Только это делается на уровне компилятора, добавлением соответствующих ключей компиляций.

    • MechanicZelenyy
      /#19253931 / -1

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

      А если серьезно, то все это выглядит так же как попытка написать объектный фортран (бессмыслено и беспощадно), чего стоят только лямбы у которых синтаксис ужасен на столько что не понятно зачем они нужны.

      • mkshma
        /#19258559

        чего стоят только лямбы у которых синтаксис ужасен

        Это просто функции со списком захвата вместо имени. Не более ужасно чем все остальное в крестах.

    • Kobalt_x
      /#19253943

      Соответственно, если такая ошибка все же обнаружилась, то логичнее всего следует остановить программу и сообщить разработчику о баге.
      Ага щаз, в каком-нибудь production коде высоконагруженного сервиса или АВ какого-нибудь.
      Деление на 0, access violation, в каком-нибудь стороннем плагине или модуле вызывающемся раз в 100 лет
      В таком случае наиболее оптимальная стратегия write_dump and continue execution.

  2. NeoCode
    /#19253937

    Да все просто.
    1. Ошибки в самой программе (в коде программы).
    2. Ошибки во внешнем мире (входные данные).
    Ошибок в программе быть не должно — но если они есть и их удалось словить, то лучше завершиться и отослать отчет разработчику. Потому что программа, определившая такую ошибку внутри себя, не может уже доверять самой себе и рисковать данными пользователя.
    Ошибки во внешнем мире могут быть — например нет сети, места на диске, ошибка формата открываемого файла и т.д. — тут нужно просто сказать пользователю. Программа тут не при чем — значит можно продолжать работать.
    Деление на ноль к примеру может быть в обоих случаях: и ошибкой в программе, и ошибкой данных из внешнего мира.

    • gridem
      /#19253973

      Конечно, все просто.


      Ошибок в программе быть не должно

      Всегда об этом говорю своей команде. А они продолжают писать с ошибками. Видимо, надо чаще об этом говорить.


      но если они есть и их удалось словить, то лучше завершиться и отослать отчет разработчику.

      Так и вижу: сервис, который работает 24/7/365 внезапно складывается и отсылает отчет разработчику, что он ленивая жопа и не хочет фиксить баги.


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

      Сильное утверждение. Проверять его, я конечно же, не буду.


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

      Ага, пример в статье прошел мимо.

      • RPG18
        /#19253979

        Так и вижу: сервис, который работает 24/7/365 внезапно складывается и отсылает отчет разработчику, что он ленивая жопа и не хочет фиксить баги.

        Почти. У нас сервисы на C++ отсылают минидампы.

        • NeoCode
          /#19254017

          Ну «отсылать разработчику» это в идеальном случае. Можно и не отсылать. Но если в программе возникло исключение, виной которому не являются внешние обстоятельства — то что можно сделать?
          Самое простое — завершить работу.
          Хорошо сформировать отчет об ошибке и попытаться сохранить данные пользователя (но тут тоже не все так просто: внутренняя структура программы может быть уже повреждена, поэтому гарантий корректного сохранения уже нет; сохранять нужно куда-то в отдельное место, не затирая старых данных).
          Если в программе хорошая модульность (например всякие плагины/расширения, хотя и необязательно), то можно попробовать отключить модуль, вызывавший фатальное исключнение, не завершая работу программы, и позволить пользователю пользоваться остальной функциональностью программы без сбойного модуля.

          • RPG18
            /#19254375

            Можно и не отсылать.

            Можно уволится и найти другую работу. К чему это. Бизнес несет репарационные и финансовые потери. Когда эти потери существенны, бизнес начинает дрючить разработчиков.


            Необработанные исключения видны в минидампе, а все остальное вынесено на уровень архитектуры.

  3. RPG18
    /#19253977

    Деление на 0. Интересно, почему эта ошибка является фатальной?

    Есть архитектуры на которых нет нормальной поддержки исключений, а при делении на 0, происходит падение.

    • gridem
      /#19254225

      Мне всегда казалось, что разделение ошибки на фатальную/не фатальную решает программист, а не процессор.

      • RPG18
        /#19254419

        Я не говорил, про процессор. Выстрелило при портирования на wasm

        • gridem
          /#19254441

          Заменим процессор на архитектуру и поймем, что сила моего утверждения осталась неизменной.

  4. mayorovp
    /#19254277

    С разыменованием нулевого указателя тут не все так просто. В той же Java есть гарантия, что разыменование null всегда приводит к исключению.

    А в С++ такого нет. В С++ это UB. Код оптимизируется исходя из предположения что разыменования нулевого указателя никогда не происходит, а ситуациях когда оно таки происходит — поведение кода в результате оптимизации может измениться. То есть при разыменовании нулевого указателя может возникнуть исключение, а может вместо этого программа сделает какую-нибудь ерунду, и это никак не отловить. А потому и писать специальную обработку таких ситуаций несколько бессмысленно.

    • gridem
      /#19254301

      Категоричность подобных утверждений всегда вызывала у меня вопросы.


      То есть при разыменовании нулевого указателя может возникнуть исключение

      Предположим.


      а может вместо этого программа сделает какую-нибудь ерунду

      А может и не сделает.


      и это никак не отловить.

      Отловить ерунду или исключение? Вот системный вызов завершился ошибкой. Программа потом может сделать ерунду и не отловить ее. Звучит бредово, правда? Так же, как и с нулевым указателем.

      • mayorovp
        /#19254365

        Отловить разыменование нулевого указателя. В исходном коде оно может быть — а в итоговой программе вместо этого будет операция «сделать ерунду».

        Как результат — программист на С++ должен всегда знать может ли в переменной быть нулевой указатель, и не имеет права полагаться на исключение.

        • gridem
          /#19257329

          В исходном коде оно может быть — а в итоговой программе вместо этого будет операция «сделать ерунду».

          Хочется понять обоснованность данной посылки. Всегда ли справедливо такое утверждение? И почему?

          • qw1
            /#19258219

            Вы пытаетесь наделить компилятор человеческими чертами.
            Чтобы он додумал за программиста и решил, что в этом случае допущена явная ошибка и надо сгенерировать падающий код.

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

            Если вам такое поведение не нравится, нужно пользоваться компиляторами «старой школы», которые сначала наивно преобразуют код в ассемблер, а затем применяют оптимизации заменой по известным паттернам, без логического вывода. Естественно, они менее эффективны, но и неочевидные вещи себе не будут позволять.

  5. iCpu
    /#19254325 / +1

    Итак, вы сагрились на предыдущую статью за то, что автор написал, в общем-то, еретичные строки о том, что деление на ноль или разыменование нулевого указателя — это фатальные ошибки.

    Справедливо.

    Согласно стандарту, и первое, и второе — это чистой воды UB. То есть падение — это одна из возможных реакций системы. Полное игнорирование происшествия — другая. А исключение — третья. Но, так как таких исключений в стандарте нет, то из стандарта вы их и не получите. Берите из boost. Оттуда же можно получить исключения для математических операций всех цветов и размеров.

    Но то, что написали вы…
    Это тоже какая-то ересь. «Нужно делать так, как нужно. А как не нужно, делать не нужно!» Спасибо, Адмирал! Хорошо, что вы не написали свой ПРАВИЛЬНЫЙ© вариант обработки ошибок. Его тут же обоснованно бы обосрали. И хорошо ещё, если бы этот вариант -fno-exceptions пережил.

    Вы перешли на следующую ступень, которая не зависит от ЯП. Проектирование ПО. «Как писать программу так, чтобы не получить комбайн со встроенной хлебопечкой?» С этой высоты абсолютно однофигово, добавят ли монады или нет. И почти столь же однофигово, что вам будут возвращать, если вы, конечно, способны это обработать. Хорошо. Замечательно. Глубоко. Вот только причём тут предыдущая статья?

    • gridem
      /#19254425

      О, первый комментарий по делу. Обычно это редкость. В смысле, часто и первого не бывает.


      Собственно, то, что это UB — это понятно. Непонятно только то, зачем вместо того, чтобы решать реальные проблемы, заниматься рюшечками? Реальная проблема в том, что это не должно быть UB. Это неудобно, надежную программу так не напишешь.


      Зачем придумали исключения? Чтобы не париться каждый раз с проверками на ошибки. А с делением на 0 надо париться каждый раз. Так же, как и с нулевым указателем.


      Ну и главный посыл — это про то, что обработка ошибок — это не цель. И отсюда исходит топтание на месте.


      А если бы я разбирал каждый абзац, то было бы как с предыдущей статьей.

      • iCpu
        /#19254853

        Может, вы слишком сильно зациклились на своей архитектуре? Напомню, C++ не только на AMD64 существует.

        Реальная проблема в том, что это не должно быть UB.
        Хм?
        На AVR я могу разыменовать 0x0000, ибо это валидный адрес для ROM'а. На нём же я могу делить на ноль без падений хотя бы потому, что AVR не может упасть. Это микроконтроллер, а не кирпич. Если у вас есть более ёмкий термин для такого поведения, чем UB, — пожалуйста, предложите его. Ну и так, до кучи, результат деления целого на ноль — целочисленный NaN. Валидное значение для математики. Но, так как его реализации нет на большинстве архитектур, он не описан в стандарте, просто упоминается вскользь. А поведение разнится от пропуска операции до системного прерывания.

        А что касается остального, да, всё верно. Только всё ваше нытьё в комментариях блокируется простым
        Уважайте своего пользователя, не будьте мудаками! Пишите понятные интерфейсы с ожидаемым поведением Проверяйте все входные параметры на валидность при входе в функцию и правильно описывайте ошибки, чтобы ему не нужно было читать комментарии и материться.
        ИМХО, должно было получиться обидно. Ну а как иначе? Вы сами написали в статье, что корректность состояния должна задаваться программистом, а не языком. У вас явная гарантия отсутствия однообразного поведения в разных условиях, описанная в стандарте. Ещё раз. Вам явно пишут, что результат операции неизвестен, так как зависит от программистов архитектуры+компилятора. Но нет, вы не довольны. И где ваша свобода выбора?

        • gridem
          /#19257323

          Может, вы слишком сильно зациклились на своей архитектуре? Напомню, C++ не только на AMD64 существует.

          Нет.


          Хм? На AVR я могу разыменовать 0x0000,

          А при чем тут нулевой адрес и ошибка с нулевым указателем? Когда я говорю про NullPointerException, я имею в виду nullptr, числовое представление которого может быть любым.


          Вы сами написали в статье, что корректность состояния должна задаваться программистом, а не языком.

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


          Вам явно пишут, что результат операции неизвестен, так как зависит от программистов архитектуры+компилятора.

          Похоже, что некоторые вещи тяжело доходят. Вот я напишу в стандарте, что для A+B результат UB. Напишу это явно, большими буквами. Чтобы не было иллюзий. Хорошо будет разработчику? Отнюдь.


          Короче, я хотел сказать несколько другое. Кто хотел понять, тот уже понял.

          • qw1
            /#19258249

            Проблема тут в том, что язык не помогает, а вставляет палки в колеса.
            Тут существует дилемма.
            1. Либо ввести в язык некоторые правила (типа, знаковое переполнение недопустимо, и разыменовывание nullptr тоже), и за счёт этого поднять качество оптимизации на мизерные проценты. Но при этом порог вхождения поднимется.
            2. Либо программисту не потребуется серьёзной подготовки, а качество оптимизации останется на уровне 90-х.

            Создатели компиляторов выбрали п.1, и наслаждаются своими логическими заморочками в оптимизаторе, а вам (как руководителю?) ближе п.2, потому что это ведёт к меньшему количеству факапов, а небольшое снижение скорости кода вам вообще не важно.

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

            • gridem
              /#19258355

              Я исхожу из "язык для программиста". К сожалению, то, что написано выше, приходит к "программист для языка". Что меня печалит.

              • qw1
                /#19258555

                Нет, программист не может быть для языка. Всегда — язык для программиста.

                Вопрос только в целях этого программиста. Решить прикладную задачу, или развлечься этакой головоломкой и получить удовольствие от того, какой ты крутой и пишешь самый быстрый (и корректный, понимая все UB) код. Или вот ещё пример — «упороться хаскелем», как говорит тут один товарищ.

          • iCpu
            /#19259165

            Да, другое… Старый добрый ной прикладного программиста про фишки языка общего назначения с дырками под системные нужды.

            Вы на секунду притормозите и задумчиво ответьте на 3 вопроса:
            На всех ли платформах, в которые может C++, можно реализовать исключения, и разумно ли это?
            На всех ли платформах, в которые может C++, возможно динамическое выделение памяти и существуют new, delete и nullptr?
            На всех ли платформах, в которые может C++, существует операция деления?

            Ответы на эти 3 вопроса очень сильно помогут вам понять, почему вы и qw1 уводите обсуждение не в ту сторону, из которой исходит проблема. Давайте лучше пообсуждаем, довольны ли мы тем, что C++ до сих пор сражается за свою универсальность.

            P.S. vintage, я говорил о математике как библиотеке языка программирования, а не о талмудах, которыми пятиклассники друг друга избивают. Извините, если ранил ваши религиозные чувства.

        • vintage
          /#19258343

          Ну и так, до кучи, результат деления целого на ноль — целочисленный NaN. Валидное значение для математики.

          Цитату из учебника по математике в студию.

      • Amomum
        /#19254855

        Конечно, обработка ошибок — это не цель. Основная цель — чтобы все работало.
        Но внутри этой цели есть всякие мелкие подзадачи, например, чтобы код было легко поддерживать, легко читать и приятно писать. Этому и посвящена упомянутая статья.

        А насчет разыменования нулевого указателя… в микроконтроллерах, например, это может быть просто совершенно обычное поведение, есть ячейка памяти с адресом ноль и в нее можно писать (и читать) вполне штатно.

        Реальная проблема в том, что это не должно быть UB. Это неудобно, надежную программу так не напишешь.

        Все UB все равно не уберешь из программы, как ни старайся. UB — это то, что происходит, когда вы нарушаете некое обещание.
        Например, ожидает функция на вход нуль-терминированную строку, а вы нуль-терминатор забыли. И что делать бедной функции? Она не может проверить, что в строке нет терминатора, если его нет. Окей, представим, что может — она должна это делать каждый раз?

        Окей, ладно, со строкой слишком просто. Представим, что у вас есть функция поиска по ациклическому графу. Предполагается, что граф ациклический — но что будет, если вы передадите туда граф с циклом?

        У всего UB логика та же самая — предполагается, что программист уже сделал все нужные проверки и соблюдает некий контракт. Компилятор считает, что контракт соблюдается, и на основе этого может оптимизировать программу.

        Вот, к примеру, godbolt.org/z/9Qa0hc — тот факт, что переполнение знаковых целых это UB (а значит, этого никогда не происходит) позволяет компилятору вообще убрать сравнение.

  6. kosmos89
    /#19258635

    Ожидаемая ошибка — ошибка, реакцию на которую предусмотрел программист. Он четко понимает, какие последствия несет эта ошибка и как ее обрабатывать.
    Фатальная ошибка, соответственно, — ошибка, которую программист не предвидел, просто забил (- Еб*анет! — Не должно...), не продумал на нее реакцию. Или решил, что ошибка действительно фатальная и продолжать смысла нет.

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

  7. mwizard
    /#19258669

    Я бы предложил разделение ошибок (не исключений) на три класса в зависимости от того, что можно с этой ошибкой сделать, и кто в ней виноват. Например,

    1. Нарушение пред-условий — внешний мир попросил фигню.

    Мы — библиотека с функцией `createUser`, и ее вызвали с недопустимыми знаками в имени пользователя. Мы — Calculator-as-a-Service, и нас попросили поделить на ноль. Мы — драйвер TCP, и нам прислали сегмент с seqnum за пределами окна приема.

    Виноваты ли мы в этой ошибке? Нет. Может ли повторение операции проверки данных, кода, который обнаружил ошибку, привести к другому результату? Бесполезно, чистые функции на то и чистые.

    Значит, нам нужно на некорректный запрос вернуть корректный ответ — выбросить исключение на неправильное имя пользователя, вернуть HTTP 400 Bad Request в ответ на деление на ноль, и отправить RST в ответ на запоздалый сегмент.

    2. Нарушение пост-условий — внешний мир ответил фигню.

    HTTP-запрос отвалился по таймауту. При попытке открыть файл нам говорят EAGAIN или «device is busy». Мы скачиваем страницу, а там неразбираемая белиберда вместо содержимого.

    Виноваты ли мы в этой ошибке? Нет. Может ли повторение операции (запрос внешнего ресурса) привести к другому результату? Да.

    Значит, нам нужно выбросить исключение, которое имеет общепринятый способ обработки, или повторить операцию энное количество раз с учетом rate limit-ов, или перепоставить операцию в очередь, или вывести интерактивное окно с вопросом «ну что, еще разок?».

    3. Нарушение инварианта — мы сотворили фигню.

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

    Мы виноваты? Да. Может ли повторение операции привести к другому исходу? Нет, так как все поведение нечистых функций, работающих с внешним миром, закрыто обработкой ошибок из пп.1 и 2.

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

    • Ryppka
      /#19259873

      Плюсовать не могу, но очень грамотно изложено. Разве что 3 вариант нельзя обработать не только в реалтайме, но и вообще в рантайме, описались, наверное?

      • mwizard
        /#19259931

        В рантайме, да, пардон — реалтайм в этому отношения не имеет ни малейшего.