Pull to refresh

Comments 70

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

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


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

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

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


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

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

throw new ...


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


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

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

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

Это просто функции со списком захвата вместо имени. Не более ужасно чем все остальное в крестах.
Соответственно, если такая ошибка все же обнаружилась, то логичнее всего следует остановить программу и сообщить разработчику о баге.
Ага щаз, в каком-нибудь production коде высоконагруженного сервиса или АВ какого-нибудь.
Деление на 0, access violation, в каком-нибудь стороннем плагине или модуле вызывающемся раз в 100 лет
В таком случае наиболее оптимальная стратегия write_dump and continue execution.
Да все просто.
1. Ошибки в самой программе (в коде программы).
2. Ошибки во внешнем мире (входные данные).
Ошибок в программе быть не должно — но если они есть и их удалось словить, то лучше завершиться и отослать отчет разработчику. Потому что программа, определившая такую ошибку внутри себя, не может уже доверять самой себе и рисковать данными пользователя.
Ошибки во внешнем мире могут быть — например нет сети, места на диске, ошибка формата открываемого файла и т.д. — тут нужно просто сказать пользователю. Программа тут не при чем — значит можно продолжать работать.
Деление на ноль к примеру может быть в обоих случаях: и ошибкой в программе, и ошибкой данных из внешнего мира.

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


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

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


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

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


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

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


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

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

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

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

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

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


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

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

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

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

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

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

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

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

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


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

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


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

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


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

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

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

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

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

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

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

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

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

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

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

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

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


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


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


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


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

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

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

Нет.


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

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


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

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


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

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


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

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

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

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

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

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

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

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

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

UPD1: Алсо, я понимаю, чем тот же злосчастный nullptr_t отличается от T* на этапе компиляции. А в рантайме? Как это всё должно выглядеть в рантайме? Как в java: указатель и (утрирую) килобайт метаданных сверху?

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

3. Эти ошибки нельзя ловить ни для каких целей, кроме протоколирования, и запрещено игнорировать. В идеале это один глобальный обработчик, который сохраняет требуемую диагностическую информацию, после чего с чистой совестью падает.
Насчёт п.2 что-то я не уверен. Например, мы пишет http-клиент: получаем url, отдаём массив байт. И как тут можно обработать тайм-аут соединения, не сообщая об этом вызывающему коду?

Ну я вижу два варианта решения данной ситуации.


Первый — это вывалить наружу "кишки", т.е. просто не обрабатывать ошибки типа ETIMEDOUT или какого-нибудь socket.gaierror, а просто передать их наружу вызывающей стороне. Минус такого подхода — если мы реализуем что-то посложнее http-клиента (например, какой-нибудь ORM, который может работать сразу с кучей разных БД), мы утекаем наружу детали реализации, и делаем эти детали реализации неявной частью контракта. В будущем мы не сможем просто взять и прозрачно перейти с BSD sockets на какой-нибудь WinSock, т.к. код вызывающей стороны уже рассчитывает получать исключения вполне определенного образца. На мой взгляд, это проблема.


Поэтому альтернативным решением может быть обработка того же таймаута внутри клиента. В самом простом случае "обработка" будет заключаться только в том, что мы перевыбросим исключение о таймауте, но уже обернув его в наш объект исключения (или вернув какой-нибудь наш собственный retval_t и т.д.). Это позволит авторам библиотеки менять реализацию с сохранением интерфейса.


В более сложном случае, http-клиент может иметь какую-нибудь условную настройку аля retriesOnConnectionFailure=5, и только затем выплевывать TimeoutException, если эти пять попыток не увенчались успехом.

Ага, я понял вашу позицию — на каждом уровне перезаворачивать ошибки в новые типы исключений / коды возврата. Если ещё для диагностики сохранять исходную ошибку в стиле C#' InnerException — будет вообще идеально.
В таком случае, на мой взгляд, желательно сохранить информацию об изначальном исключении, но не давать к нему программного доступа — т.е. выводить только где-нибудь в стектрейсе. Как раз чтобы избежать ситуации, когда пользователь библиотеки привяжется к деталям реализации, но при этом позволить в ручном режиме диагностировать возможные проблемы с внешним миром.
А вы не думали, что это не ваша зона ответственности решать на что и как пользователь будет привязываться?
А вы не думали, что стоит отказаться от private и protected, ведь тут все вокруг consenting adults? Что сделать частью публичного контракта детали реализации, которые поменяются в следующем же коммите — это восхитительная идея, которая дисциплинирует авторов и учит их не менять многое?

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

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

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

Те же, кто специально захочет странного — форкнут и поменяют саму библиотеку, без проблем. Но в таком случае любые заковыки с будущими версиями, которые неизбежно возникнут, более не будут моей зоной ответственности.
А вы не думали, что стоит отказаться от private и protected.

У нас в JS их отродясь не было. И знаете, живём как-то. Как же это мы так справляемся, а?


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

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


Т.е. моя цель — избежать ненамеренно неправильного использования

Это замечательно. Вы вернули свою обёртку. Непреднамеренно воспользоваться низкоуровневым исключением уже не получится. Только преднамеренно.


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

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


кто специально захочет странного — форкнут и поменяют саму библиотеку, без проблем

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


распределенная консистентность, идемпотентность, масштабируемость

Чёт вы далеко улетели. Мы говорили про исключения. Давайте не растекаться сферическими мыслями по вакуумному дереву.


Вот вам несколько обыденных вещей на подумать:


  1. Нужно вывести пользователю понятное сообщение о случившейся неприятности, не вываливая на него стектрейс. Например: "Не удалось авторизоваться, так как не удалось изменить файл cookies/ya.ru.txt, так как он открыт на чтение процессом vscode.exe".


  2. В библиотеку в той или иной форме передаётся колбэк, в котором бросается кастомное исключение. При выходе из библиотеки неплохо бы получить это самое исключение, а не обёртку.


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


А чья же? Кто, кроме разработчика библиотеки, может решать, где чья зона ответственности в его библиотеке?
Если дать пользователю свободу, будет та же фигня, что с WinAPI. С тем самым, который в Win10 и Wine эмулируют баг-в-баг, потому что иначе половина софта не работает. Потому что пользователи привязались к незадокументированным функциям или ошибочному поведению вместо того, чтобы чётко следовать оговоренному в документации контракту. (Хотя, согласен, документация там тоже не самая удачная.)
Кто, кроме разработчика библиотеки, может решать, где чья зона ответственности в его библиотеке?

Кто кроме разработчика приложения, может решать вообще хоть что-то в его приложении?


С тем самым, который в Win10 и Wine эмулируют баг-в-баг, потому что иначе половина софта не работает.

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

Кто кроме разработчика приложения, может решать вообще хоть что-то в его приложении?
А что разработчик вообще может решать в своём приложении? Вы, такие умные, на JS можете сказать хромиуму уйти на йух со своими манифестами и разрешениями? Или без разрешения этой среды-библиотеки вы даже пукнуть не смеете? Можете не отвечать, это риторический вопрос.
А вы бы предпочли, чтобы эти приложения вообще никогда не работали? Эти недокументированные апи использовались не от хорошей жизни, а потому что без них было не обойтись на тот момент.
Или можно было, пожертвовав частью функционала или толикой производительности. Собственно, так и пишут те же сайты для IE.
Но есть у нас, например, особенные сайты, которые используют дырки движка IE, ActiveX и плагины для обеспечения защищённого соединения с тем же банком. Ну же, скажите, «у них не было выбора», «либо так, либо никак», «не всем можно мазилу» и «а вы бы предпочли, чтобы у них вообще не было вебморды». Да, я бы предпочёл без вебморды, чем ту, которая не работает.
Вы, такие умные, на JS можете сказать хромиуму уйти на йух со своими манифестами и разрешениями?

Можем, если пилим десктопное приложение.


особенные сайты, которые используют дырки движка IE

Дырки в чём бы то ни было к обсуждаемому вопросу отношения не имеют.


ActiveX и плагины

Напомню, что впервые XMLHttpRequest появился именно в IE и именно в виде ActiveX объекта. Когда там в браузерах криптография появилась я не засекал, но тоже сильно позже, чем она была доступна через ActiveX.

Рад что моя статья понравилась и побудила написать этот пост!

Считаю что «фатализм» это удобно. В новых мейнстримных языках go/rust/swift есть механизмы для unrecoverable error.

Да. Можно свести все к исключениям. Но, кажется, понятности это не добавит, лишь повлечёт неявную логику для таких исключений в catch(...).

Тот факт, что в других языках что-то сделано не говорит о том, что оно сделано правильно.


Все дело в безопасности. Я бы хотел иметь контроль над тем, что происходит при вычислениях и преобразованиях, а именно:


  1. Возможность выбирать между быстротой и безопасностью.
  2. Выделять явно куски кода, где я хочу разных гарантий.
  3. Иметь иерархическую модель обработки исключений.

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


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

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

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


С++ потихоньку вбирает в себя фичи которые прижились в других языках, старается не ломать то что уже есть и не замедлять производительность неиспользуемой функциональностью языка.
ИМХО — адекватный подход.


По пунктам:


1. Возможность выбирать между быстротой и безопасностью.

Пожалуйста, выбирай! кто ж мешает. Напиши class safe_int и дели на 0.


2. Выделять явно куски кода, где я хочу разных гарантий.

Кажется noexcept, [[nodiscard]], контракты и концепты как раз движение в эту сторону.


3. Иметь иерархическую модель обработки исключений.

Исключения не панацея. У них есть уйма минусов с которыми нужно считаться. Не всем они подходят.

У них есть уйма минусов с которыми нужно считаться. Не всем они подходят.

Хочу увидеть эту уйму. Хотя бы пяток, но лучше десяток. Мне кажется, что "уйма" подразумевает немалое количество пунктов.

Ну зачем так горячится? Достаточно одного: неопределенная верхняя граница времени раскрутки стека. Но может и продолжить: нелокальность, трудности при разработке с exception safety, препятствия при прохождения через «чужие» функции. Наверное, более знающие, чем я, люди могут продолжить.
Я тоже все не вспомню. Но вот ещё 3 из моего поста:
1. по сигнатуре функции невозможно понять какое исключение может вылететь из функции.
2. размер бинарного файла увеличивается за счёт дополнительного кода поддержки исключений.
3. нельзя выбрасывать исключения из деструкторов.

Из комментария выше ещё 3:
4. долгая раскрутка стека и не соблюдение принципа «не плати за то что не используешь».
5. несовместимость с «чужими функциями».
6. нелокальнось (код обработки ошибки может быть далеко от того места где она произошла).

Ещё из практики:
7. на некоторых платформах, например новом android ndk и clang нельзя полагаться на исключения между .so на версиях ниже 5.
8. необходимо генерировать `typeid` и экспортировать его из .so для всех типов Увеличивает время линковки.
9. если не соблюдать RAII, то исключения легко ведут к утечке ресурсов, появляются неявные выходы из функций.
10. исключения тяжело внедрять постепенно в legacy код, который изначально не написан exception safety.

Вот. Ровно 10. Хотя п7-8 это особенности реализации.

Пройдусь немножко:


1) по сигнатуре функции невозможно понять какое исключение может вылететь из функции.

Это проблема языка, а не исключений как таковых. Есть noexcept, которое говорит о том, что исключения не полетят.


2) размер бинарного файла увеличивается за счёт дополнительного кода поддержки исключений.

Да, это — плата, и это в некоторых случаях может стать решающим.


3) нельзя выбрасывать исключения из деструкторов.

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


4) долгая раскрутка стека и не соблюдение принципа «не плати за то что не используешь».

Если исключение не летит, то за это ты не платишь. Поэтому не очень понятно, что тут имеется в виду.


5) несовместимость с «чужими функциями».

Это я не очень понял. Можно раскрыть мысль?


6) нелокальнось (код обработки ошибки может быть далеко от того места где она произошла).

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


7) на некоторых платформах, например новом android ndk и clang нельзя полагаться на исключения между .so на версиях ниже 5.

Не является проблемой языка, и исключений в частности.


8) необходимо генерировать typeid и экспортировать его из .so для всех типов Увеличивает время линковки.

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


9) если не соблюдать RAII, то исключения легко ведут к утечке ресурсов, появляются неявные выходы из функций.

Если не соблюдать RAII, то будет больно при разработке на C++. Любая аллокация памяти может кинуть исключение, и нужно писать специальным образом, чтобы это не происходило. Поэтому надо соблюдать RAII вне зависимости от того, используются исключения или нет.


10) исключения тяжело внедрять постепенно в legacy код, который изначально не написан exception safety.

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


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

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

Если деструктор был вызван из-за исключения, то появление нового исключения ведет к UB.

Мне всегда казалось, что вызывается std::terminate, т.е. никакого UB. Даже если UB, явного запрета нигде нет.

А, точно. Но от программы требуется-то, чтобы она работала, а не завершалась. Так что практически std::terminate при двойных исключениях эквивалентен запрету выкидывать исключения из деструкторов.
1) Так-то есть T foo() throw(Types,...);, которой никто не пользуется и которая уже обозначена устаревшей.
2) Зависит от реализации компилятора. Некоторые просто делают условный переход, особенно, на микроконтроллерах. Там, где механизм исключений введён, работает и не транслируется в /dev/null.
5) Вероятно, имелось в виду, что если исключение, которое брошено тебе, пролезает через некоторую функцию, нет гарантии, что оно до тебя дойдёт, спасибо catch(...)
11*) Исключения не могут выполнять ту же функцию, которую на них накладывают в других языках из-за оптимизирующего компилятора. В частности, стек результирующего кода чаще всего не эквивалентен вашим исходникам, да и порядок операций не обязан совпадать. Учитывая, что оптимизирующий компилятор является чуть ли не фишкой языка (не является, но что такое плюсы без -O3?), этот пункт не стоит списывать в «реализацию» или «придирки». Хотя да, локализация ошибки — это не основное назначение исключений, так что пункт под звёздочкой.
12) Расширение списка стандартных исключений серьёзно ломает обратную совместимость языка и увеличивает накладные расходы. Например, исключение по целочисленному переполнению добавляет по одному условному переходу к каждой операции +,-,*. Код для них должен увеличиться на порядок, с одной-двух операций до пяти-дюжины на разных системах. Эта та цена, которую вы требуете, но которую я не желаю платить, потому что я сам способен проверить их на входе.

Собственно, из пункта 12 (и 2) и исходит неоднократно предложенное вам решение использовать уже написанные и доступные из буста тип safe или библиотеку математических исключений.
А процессор не умеет вызывать прерывание в случае переполнения, деления на ноль и тп?

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


Проблема в том, что разработчики с++ заявляют: мы о вас подумали и вы платить не будете. А чтобы было веселее, мы добавили вам UB. Т.е. по факту:


  1. Либо есть плата в рантайме. Опять же, никто не измерял, все лишь говорят о ней. Предсказатель переходов для горячего кода должен вполне неплохо с этим справляться.
  2. Либо есть плата из-за того, что может быть UB. Т.е. корректность положена на плечи разработчиков. Это то, что должен каждый знать: С++ кладет на разработчиков. Прежде всего, кладет сложность, но и не только.
Я думаю это должно решаться через систему типов. Если компилятор может вывести, что нуля быть не может, то спокойно убирает проверку. Если не может — добавляет, если программист не указал явно её убрать. Аналогично с переполнениями и прочим.
Это не будет работать, если данные поступают извне. Либо придётся явно писать валидацию данных, как хинт компилятору.

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

Не обязательно. Например, при написании игрового движка и загрузки текстур/моделей нужно ли обязательно проверять каждый пиксель и каждую вершину?
Тут же очень много всего можно вспомнить, что C++ кладёт на разработчиков. Та же валидность указателей. Вот, например, что будет, если кастануть char* в T*? Ладно, вопрос попроще, что будет, если кастануть &(char[0]) в char*? О, это же UB!!! Как так? Нельзя! Нужно запретить! Бросать исключения! Мы же можем залезть в левую память!
Звучит один-в-один как у вас. У нас же не определено, что будет, если указатель наложился на область памяти, которая не совсем класс. Которая меньше, чем класс, иначе устроена, чем класс и тд и тп. И у нас нет гарантий, что на всех платформах адреса массива и его первого элемента совпадают. И?

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

  1. Зависит от реализации. Для поддержки исключений, на сколько я знаю, в стек кладутся дополнительные данные. Это ухудшает локальность. Не пробовал считать насколько это плохо.


  2. Нет совместимости с C. Если исключение полетит через функцию скомпилированную С компилятором, то буде плохо.


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



8-9. Ну никто не придумал лучшей реализации.


  1. Ну представим что у нас есть legacy проект на С. И мы начинаем кусочками переписывать его на C++. Нужно быть предельно внимательным, если исключение пролетит через старый не RAII код, будет беда.
Sign up to leave a comment.

Articles