Исключения и goto

Среди программистов периодически разгораются холивары на тему “Использовать оператор goto в программах или нет”, хотя тема вроде как уже давно обсуждена и закрыта. Еще в 1968 году Дейкстра написал свой труд про пагубность применения этого оператора и с тех пор goto осужден практически всеми классиками и запрещен во многих стандартах кодирования (но при этом по прежнему не удален из языков).
Понятно, почему goto - это зло. Этот оператор позволяет слишком много, и его неправильное использование приносит больше вреда, чем пользы. Он разрывает целостность программы, не позволяет анализировать flow (порядок выполнения) функций, позволяет создавать неподдающиеся пониманию конструкции и т.п.
Но написать я хотел совсем не о Goto. Goto - это просто прелюдия, чтобы вы вспомнили о проблеме.


Разбирался я недавно в очередной раз с кодом, в котором было много исключений. Весь механизм обработки ошибок был построен на исключениях. Причем, не только критических ошибок, но ВСЕХ. Это ведь считается правильным подходом - если ты выбрал один из методов обработки ошибок, то его и применяй везде - либо исключения, либо возврат кодов ошибки. Вот некоторые и применяют…
Хочешь проверить наличие файла на диске - генери исключение, если файла нет.
Хочешь проверить, создан ли уже эвент - попытайся его создать и лови исключение, если он уже создан.
Не смог выделить 10 байт памяти - генери исключение, что память кончилась.
Любая вызванная WinAPI функция вернула ошибку - генери исключение типа “внутренняя ошибка Windows”.
В итоге, код весь получается усеян операторами throw, try и catch в перемешку с кучей операторов if, которые никуда при этом не деваются. Практически в каждой функции можно найти 2-3 блока обработки ошибок, которые обычно ничего не делают, кроме как пишут что-то в лог и иногда удаляют выделенную память (опять же, сначала проверив парой if-ов наверняка, а была ли она выделена).
При этом многие из исключений кидаются во вполне безобидных ситуациях, когда никаких критических ошибок нет и это нормальный путь выполнения функции. То есть, если ты поймал исключение в дебагере, то это не ошибка - они генерятся постоянно во время работы программы.


И вот, разбираясь с очередным багом в таком коде, я вдруг осознал, что исключения на самом деле недалеко ушли от оператора goto. Да, не надо мне напоминать, зачем ввели исключения в язык, что они позволяют автоматически удалять переменные и т.п. - это я всё знаю, конечно. Но посмотрите на это с другой стороны. Со стороны того, кто поддерживает такой код и должен его понять и исправить.
Итак, я открываю функцию и смотрю, что она делает. Я вижу throw. Я должен понять, что произойдет. Я должен просмотреть глазами всю функцию до конца и найти catch. Я не могу сразу смотреть в конец, т.к. catch может быть посередине. Если я нашел catch, я должен сравнить типы исключения и его ли вообще ловит этот catch. Если совпало - слава богу. Можно вернуться обратно в середину функции и изучать код дальше.
А если в функции нет catch? Тогда это исключение заставляет меня изучать все места, откуда зовется эта функция и искать catch там. А если и там нет catch? Тогда все места вызова всех тех функций!!!
Мягко говоря, это раздражает побольше, чем goto - там хотя бы можно по имени метки найти, куда будет осуществлен переход. А в случае с исключениями, ты теряешь контекст и долго ищешь, куда перейдет контекст исполнения. Но даже когда ты нашел, уверен ли ты, что это единственный путь исполнения или что, например, вся нужная память освободится? А фигушки.
В C++ гарантируется только освобождение созданных локальных переменных. Если ты вдруг использовал new - это уже опять же твоя проблема, как подчистить за собой. То же самое, что и при применении goto.
В итоге, каждый встреченный тобой при анализе функции оператор throw превращается в ненавистного врага. Ты боишься их, т.к. знаешь, сколько кода придется изучить из-за этого одного безобидного оператора, чтобы понять логику работы программы.


Можно ли, используя исключения, запутать код так, чтобы его было сложно понять? Легко. Просто используйте исключения везде. Сделайте пару сотен типов исключений и кидайте их везде. А ловите не везде, а только в некоторых функциях, чтобы сложнее было отслеживать порядок выполнения кода.
И этим вы гарантируете работу, много работы, тем, кто будет поддерживать ваш код в будущем. Но это же уже не ваша проблема…

65 комментариев к Исключения и goto

  • KmDm

    Вообще целиком и полностью согласен. Меня тоже вымораживают исключения. В Лесте никогда их не использовали - во всяком случае в Агрессии и 9-й Роте. И сейчас когда уже “приучили” к их использованию, периодически наталкиваюсь на ту же мысль что звучит в твоем посте.
    Исключения вовсе не отменяют проверки, просто с использованием исключений у тебя уже есть не одна проблема а две.
    Не говоря уже о том что в некоторых случаях - например при ошибке выделения памяти бросать исключение вообще нет смысла, т.к. для создания объекта исключения опять же нужна память.

    • При исключении памяти объект исключения можно создать в стэке, а не в куче - это решит проблему.

      • KmDm

        А точно, давно на C++ не программировал.)

      • konstantin

        Не всегда поможет, ибо есть “творческие” API, где создание объекта на стеке не гарантирует вызов специфических методов очистки при выходе за область видимости. Для примера API от Adobe для создание и редактирования PDF или API от Nuance для разпознавания и конвертации документов. :(

  • Left

    > В C++ гарантируется только освобождение созданных локальных переменных. Если ты вдруг использовал new - это уже опять же твоя проблема, как подчистить за собой. То же самое, что и при применении goto.

    Э… Смартпоинтеры запретили разве?

  • Ужас. Сам неоднократно сталкиваюсь с таким безобразием. Вместо return код_ошибки используют err = код_ошибки; goto TERMINATE;, даже когда нет никакой необходимости в обработке этой ошибки, просто возврат значения. try/throw/catch в тех местах, где можно было бы обойтись простой конструкцией if/else.

  • Почему-то комментарий был обрезан.

    • OpenID плагин обрезает комментарии, где встречается заглавная буква и. Разрабы не исправили пока что.
      Обойти можно, не начиная предложения с заглавной буквы и или зарегиться на сайте - тогда нет этой проблемы

      • Спасибо. Тогда вот исправленный комментарий.

        Ужас. Сам неоднократно сталкиваюсь с таким безобразием. Вместо return
        код_ошибки
        используют err = код_ошибки; goto
        TERMINATE;
        , даже когда нет никакой необходимости в обработке этой
        ошибки, просто возврат значения. try/throw/catch в тех
        местах, где можно было бы обойтись простой конструкцией
        if/else. Что еще хуже, throw без
        try/catch используются в деструкторах! При всем этом, весь
        остальной код не учитывает того, что где-то может возникнуть исключение (т.е.
        если исключение будет, получим множество утечек).

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

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

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

        >> В C++ гарантируется только освобождение созданных локальных переменных.
        Если ты вдруг использовал new - это уже опять же твоя проблема, как
        подчистить за собой. То же самое, что и при применении goto.
        > Э… Смартпоинтеры запретили разве?

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

        • >> Что еще хуже, throw без try/catch используются в деструкторах!

          Это говорит еще и о том, что не используется статистический анализатор кода - он такие ошибки ловит и показывает обычно. Помню я как-то занимался копипастом целый день - исправлял такие ошибки в деструкторах и вставлял туда try…catch.

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

          Интересно, а как вы в этом случае гарантируете выделение ресурсов после освобождения? Храните специальные заранее выделенные куски памяти, которые можно удалить? Или у вас что-то вроде своего сборщика мусора?

  • SWW

    Вся Windows написана на goto. При грамотном использовании goto превращается в быстрый и удобный код. Меня вот, например, вложенные if’ы (десятками, пачками просто) ужасно вымораживают.

    На самом деле, процедуры/функции должны быть маленькими, так, чтобы не приходилось листать по несколько страниц вниз. Тогда подойдут и if’ы, и goto, и __try с __finally.

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

      • SWW

        Ну, за такое надо сразу руки отрубать =) Максимум - это хендлер выше функции, но тоже не комильфо. К тому же, исключения - это довольно медленная штука, на перформанс влияет довольно сильно. Во-всяком случае, goto явно быстрее :)))

    • SS

      goto не использую, вера не позволяет :)
      Не согласен…
      Если даже код полностью “вылизан” т.е. ф-ция содержит до 25 строк кода, коментарии, вложения if менее 3-х уровней и т.д. все просто кажется отлично… Добавим маленький момент, пусть мы работаем с COM интерфейсами и функциями экспортируемыми из пусть даже своих dll-ок (скажем разрабатываемых сосоедним отделом), документации еще нет :) код постоянно меняется протокол обязывает кидать ловить эксепшены :) ой какой квест по поиску куда попадет кинутый, измененный(вчера был один для него и написали перехват, а сегодня решили что правильней кидать другой :) ) эксепшен и где и какую ошибку потом мы обработаем и как :)
      Для предлающих ловить все, скажу ни внутренний стандарт не позволяет и считается дурным тоном :)

  • — Слышал я «Битлз», не понравилось. Картавят, фальшивят… Что людям в них нравится?!
    — А где ты их слышал?
    — Да мне Рабинович пару куплетов спел…

    • Я не отрицаю полезности грамотного использования исключений в библиотеках. Но исключения плюс “спагетти код” - это настоящий программерский ад.

  • Еще Joel Spolsky писал об этом, что исключения это тотже goto…

  • ctapmex

    как говорил наш преподаватель по системному программированию - goto плохо, а что делать нам в ассемблере ? как там без goto ?

    а можете подкинуть статейку или источник где можно почитать как лучше использовать трай кэтчи или это только на опыте?

    • >> а можете подкинуть статейку или источник где можно почитать как лучше использовать трай кэтчи или это только на опыте?

      Страуструб, Мейерс. Но книги не помогут, да - нужен опыт.

  • bearded

    Мне кажется что не надо слишком рьяно использовать исключения, т.к. реально приходится в конечном итоге думать о том как бы (и где бы) его перехватить и обработать, а не над тем как это исключение избежать. Использовать или не использовать исключения должно следует следовать док-ции, если метод может выбрасывать исключениие - то на месте его ловить и “переводить” на логику программы. Если возвращает что-то типа HRESULT и устанавливает err - тогда тоже самое.

    Как грит Дж.Рихтер в книге Programming Applications for MS Windows - обработка исключений предназначена для случаев которые происходят очень редко, а для тех случаев когда исключение - это обыденность, лучше проверять явно (по памяти написал цитату). Это он сказал для SEH винды, но я думаю это актуально и для c++-ных исключений.

    Из google coding standard - “We do not use C++ exceptions.” и далее по тексту, полностью согласен с ними.

    Про goto: - очень нравится мне эта техника т.к. позволяет реально сократить код. Но только когда логика НЕ построена на goto. То есть один-два goto вполне воспринимаемо (типа нормальное/ненормальное завершение метода). А вот когда идут кросс-goto (из одного можно перескочить в другой и т.д.) - тогда можно долго просидеть разбираясь в коде.

    Очень интересно смотерть на код новобранцев (большей частью новобранцы .net), как каждый метод “обрамлен” блоком try-catch. И особенно обидно что когда-нить да встречается пустой catch - и из-за этого возникают “аномальные” проблемы и баги.

    • Да, полностью согласен. Исключения нужны ТОЛЬКО для ИСКЛЮЧИТЕЛЬНЫХ ситуаций. Они не должны встречаться в нормальном пути исполнения функции.
      Проблемы начинаются именно когда кто-то начинает их использоваться для всего, т.к. “мы выбрали исключения”. Надо миксовать подходы и использовать и возврат значений ошибок и исключения.

  • Alek86

    >> То есть, после m_clients[name]= obj, можно просто проверить, что m_clietns[name].initialized().
    а можно забыть и не проверить - и тогда программа будет работать с непроинициализированными данными

    • Alek86

      ой, не туда прислал

    • >> >> То есть, после m_clients[name]= obj, можно просто проверить, что m_clietns[name].initialized().
      >> а можно забыть и не проверить - и тогда программа будет работать с непроинициализированными данными

      Также можно забыть и catch поставить и там все откатить.

      • Alek86

        если забываешь поставить catch, то исключение полетит “наверх” - до первого встречного catch(это_исключение_или_его_предок). или завалит программу, что в общем случае лучше, чем дать ей работать с мусорными данными (что будет в случае сишного подхода + забывчивости программиста)
        при этом подразумевается, что все внутренние объекты использоали RAII и освобождали свои хенлы, выделенную память и т.п.

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

      • Alek86

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

        • Если функция сама за собой чистит, то все ок, пока не окажется, что она вызывается в контексте какого-нибудь сложного большого автоматического алгоритма. Например, при сортировке массива или хотя бы при вставке нового элемента в массив\мапу. Тут уже откат на уровне функции не поможет - вся система будет в неопределенном состоянии после исключения, ибо неизвестно в какой момент это произошло.
          Другое дело, что без исключений эту ситуацию даже поймать не получится… Так что в данном случае исключения могут быть полезны, согласен.

  • > Для сложных объектов всё-же конструктор копирования может и должен память выделять и даже ресурсы какие-то может захватывать.

    Может. В этом случае просто заворачиваем сложный объект в простой, например auto_ptr.

    • auto_ptr - это очень опасная жесть. Слишком велика возможность случайной ошибки при его использовании.
      Его из стандарта долгое время хотели исключить и вроде как исключили наконец-то.

  • На самом деле все хорошо в меру. :) Прописная истина, но все же.

    Можно слишком увлечься исключениями, и использование исключений превратиться в самоцель… Можно слишком увлечься чем угодно… :)

    Я видел код на “си” (dll для Windows Crypto API) В котором активно использовались исключения… Причем видимо автора очень напрягало писать if (…) throw, (вместо … обычно вызов Windows API функции) - и он для этой цели завел макросы: throw_error_if()…. точно уже не помню… макросы были на все случаи жизни, вообще весело.. и естественно все исключения на выходе преобразовывались в коды ошибок. :)

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

    А вот необходимость в goto я лично вообще не испытываю, особенно если не пытаться в одной функции сделать несколько дел.

  • Xor

    Пока что у меня в коде (.NET) получается примерно следующее:
    - Если у нас есть простая операция, которая может быть только true/false, используем возвращаемое значение.
    - Если операция может провалиться по нескольким причинам, используются исключения. При этом код стараюсь максимально документировать по исключениям.
    Речь про внутреннюю библиотеку классов. Вот сейчас думаю, насколько это правильно :)

    p.s. понятно, что на С++ всё сильно по-другому, и такой подход, вполне возможно - в большей степени чреват ошибками.

    • konstantin

      Вы таки будете смеяться, но стратегия использования исключений не зависит от языка (точнее зависит от того поддерживаются ли исключения языком или нет). ;)
      У Майерса и Эккеля в их книгах не по С++, а по программированию вообще, описывалась следующая стратегия применения: для своей логики используем коды возврата ошибок, для взаиодействия с внешними системами используем исключения.

      Концентрат этой стартегии к себе утащил как-то: proceedingtothetop.blogspot.com/2009/06/blog-post_30.html

    • alex

      однозначно нет, тем более в .нет
      задайте вопрос себе - а зачем они нужны, исключения? ответив на него себе сразу 80% исключений из кода выбрасываются.
      не говоря уже о том, что исключения - очень дорогостоящая операция.

      • SS

        Да зачем повторять чью то “авторитетную” фразу?
        Напишите тест за 30 минут с нормальным тамером, сделайте 20-50 замеров и убедитесь что время выполнения и на 3% не увеличится!!! Разность измерений 1-2% входят в порядок погрешности измерений. Только не забывайте что система живет своей жизнью поэтому постарайтесь составить тест так чтобы минимизировать влияние внешних для вашей программы факторов.

  • Вообще, Джоэль уже неимоверно давно поднимал этот вопрос. Так что тема сходства исключений и goto обсосана со всех сторон. Моё имхо - при правильном дизайне исключения более чем отличная и понятная штука.

  • alex

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

    эээ…. это кем считается-то?

    • >> эээ…. это кем считается-то?

      Книги про это написаны и статьи.

      • alex

        хе-хе, в книгах много чего написано. только если по книгам делать - ничего работать не будет. как пример - кто нормализует базу до первой формы?
        а в книгах пишут, да-да…
        в код гигантов (МС, Сан, ИВМ etc) куда полезней смотреть, а не в теоретические книги. потому что опыт, а не теория.

  • Всёравно по ситуации нужно смотреть, понятно дело не всегда его использовать, но бывают моменты, когда надо.

  • явачспмит

    не уверен, что я правильно понял автора, но по моему все описанные подходы неверные. вопрос использовать исключения или нет при нормальном, в моем понимании, дизайне не стоИт. это очевидно из назначения функции. если сформулировать в виде правил, то получится что-то вроде:
    1. Функция делает одну и только одну вещь, ради чего ее и писали.
    2. Функция возвращает РЕЗУЛЬТАТ и только результат своей работы. Результат - это то, что ожидает пользователь, то ради чего он ее вызывал; если он ничего не ожидает то ф-ция возвращает void.
    3. Результатом может быть и ошибка, в том случае если это единственное что возвращает функция. пример - GetLastError
    4. Если ф-ция не может вернуть результат она должна кинуть исключение. Возвращать ВМЕСТО результата или void код ошибки практика порочная, заканчивающийся всегда одним и тем же. Либо коды никто не проверяет, либо код превращается в спагетти. Если необходимо избежать исключений то надо не городить огород с кодами возврата и построении логики на приключениях, а сделать парную функцию которая проверит возможность успешного выполнения. Ну самый тупой пример это if (!File.Exists(”myfile.txt”)) CreateFile(”myfile.txt”);
    в таком вот духе…

  • deadline

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

    Для начала немного предисториии.

    В свое время бежал от исключений как от огня, ибо разработчики VCL ( delphi ) поступили также тупо, как описано в примере автора - “Хочешь проверить наличие файла на диске - генери исключение, если файла нет”. Причем таких дебильных примеров использования исключений было настолько много, что ответная реакция со стороны нашей команды не заставила себя ждать - переход на WinAPI функции( что тоже не везде гладко получилось ) и ни одного raise в своем коде. При этом мы забывали проверять результат вызываемых функций, но радостно жевали этот кактус. Против делфи могу много чего сказать, и сказал на rsdn в свое время, но сейчас холиварить на эту тему не собираюсь.

    Затем я перешел на коммерческую разработку с использованием с++ и заметил что использование исключений в stl абсолютно не мешает мне жить! Как же так, ведь тут столько проблем описано!? :) Может все таки дело не в самих исключениях, а в кривизне рук тех, кто их применяет?! Аналогично мне не мешают жить исключения в бусте. Зато я уверен в том, что возникшая проблема не будет проигнорирована, а для меня это выражается в $$.

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

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

    > Можно ли, используя исключения, запутать код так, чтобы его было сложно понять? Легко. Просто используйте исключения везде.
    Абсолютно согласен. А еще я могу запутать код тупым юзанием пойнтеров - откажемся от указателей? О, да я могу еще типа такого писать в for - :^?/%:*? ( образно конечно). Откажемся от всех операторов? Да я и сам могу написать на с++ вполне корректный код, переваривание которого мозги расплавит моск и меня отправят читать книжку по рефакторингу, не отказываться же от всего с++ только потому что руки, пардон, из задницы растут.

    > И этим вы гарантируете работу, много работы, тем, кто будет поддерживать ваш код в будущем. Но это же уже не ваша проблема…
    Ну, если писать на с++ как в басне Крылова “Мартышка и очки”, то так и будет. И исключения тут не причем, тут будет системная проблема кривых рук. А сколько интересных проблем выплывет на указателях. Я все же рекомендую изучить инструмент, на котором пишешь, и писать правильно, а не подымать ор, что “очки не работают”.

    > Хочу тест написать и сравнить влияние try…catch на перформанс. Если время на выходных найду, то займусь и опубликую.
    Отлично! Я так понимаю о “преждевременной оптимизации” ни сном ни духом. Гонка за производительностью безусловно интересна … но только в тех случаях, где она реально обоснована. К примеру у меня нет ни малейшего желания оптимизировать операции целого модуля, если запросы к базе данных в нем отнимают 99,9999% процентов времени. А в случае исключений так и вообще - достаточно правильно ими пользоваться, чтобы не возникал вопрос потери производительности. Я напоминаю, исключения это ИСКЛЮЧИТЕЛЬНЫЕ СИТУАЦИИ, а не workflow.

    > функция, бросающая исключения должна сама за собой подчищать, а не оставлять эту заботу вызывающему коду (хотя бы во избежания копипаста)
    Это верно, но только для случая с интерфейсными функциями. В остальных случаях “must” меняется на “should”, ибо оверхед.

    P.S. Хочу поблагодарить eol197 ( кажется мы с ним встречали уже на просторах rsdn ) за подъем насущных проблем, возникающих при использовании исключений. Абсолютно и полностью разделяю его мнение по поводу “кривых рук”. Остальным противникам исключчений рекомендую читать умные книжки до тех пор, пока не появятся убедительные аргументы против исключений, а может и осознанное желание смены языка. Не получается грамотно писать на с++ - используйте C# или java - эти языки куда проще.

    P.P.S. Время от времени всплывает аргумент по типу “исключения засоряют код”. Но я вас уверяю, постоянные if засоряют его не меньше, а лично мой опыт подсказывает что if засоряет код даже больше, ибо один единственный catch ( я не про пустой ) в интерфейсной функции заменяет таки тонну if-ов.


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

      Аналогично. Поэтому советую перечитать еще раз мой пост и попытаться там найти требование не использовать исключения. Я писал про исключительные ситуации в использовании исключений. Про их необдуманное использование. Как раз про тех, кто не читал Саттера и иже с ним.
      И, собственно, статья не про вред исключений, а про сравнение исключений с goto. Сравнение вполне валидное и не я первый-не я последний.

      >>Я все же рекомендую просмотреть саттера и правила использования исключений, чтобы не орать потом о потерях производительности, вызванных подобными ожиданиями.
      >>Строить workflow на exceptions это нонсенс.

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

      >>Строить workflow на exceptions это нонсенс.
      Собственно, в этой фразе вся моя статья, только я расписал про это гораздо подробнее. Да, это нонсенс. Но не все это понимают.

  • MR

    По поводу “Исключения и goto”
    И саго, употребленное не в меру, может причинить вред.
    Вообще в ++ много чего есть такого без чего ему было бы не хуже. Код можно запутать и с исключениями и без.
    Так что самоограничение в коде - диета программиста.

  • Блин, уже не первый раз комментарий теряется.

    Повторю. Это проблема “кривых рук”. С накоплением опыта и количество классов исключений уменьшается, и использование throw/catch упорядочивается, и ресурсы не текут.
    Добавлю так же, что опасность исключений не столько в перечисленных в статье проблемах. А в том, что они могут прервать практически любую более-менее сложную операцию в любом месте. Поэтому актуальным становится вопрос exception safety и strong/basic-гарантий. А это посложнее невызванного delet будет.

  • >> А в том, что они могут прервать практически любую более-менее сложную операцию в любом месте.

    Как они могут прервать операцию в любом месте? Только если программист написал throw и не поймал этот throw тут же в функции - именно про эту жесть я и писал, когда приходится искать совершенно в других функциях продолжение алгоритма.

  • Элементарно. Допустим, у нас есть std::map (где в client_info_t сидят еще несколько std::string-ов). Тогда простой код по занесению нового клиента в map:

    m_clients[ client_name ] = client_info_t( params );

    может оставить m_clients в неправильном состоянии, если исключение возникнет во время копирования или даже конструирования объекта client_info_t в правой части присваивания. А возникнуть может исключение, скажем, о нехватке памяти.

  • Да, теперь понял. Интересно, как можно после этого восстановить m_clients?
    При любом исключении считать m_clients испорченным и заново в него пихать этот client_info_t?

  • Основных решения здесь два:
    - самое суровое, но и самое простое (надежное) — это откат всего модуля, в котором возникло непредусмотренное исключение до точки, в котором его можно полностью рестартовать. Тогда m_clients будет вообще создан заново;
    - если это не подходит, тогда используется принцип транзакций: либо все, либо ничего. Один способ обеспечения этого — обвязка модификации m_clients try/catch, чтобы в catch был удален ключ client_name. Второй способ — написание guard-а, который в своем деструкторе сделает тоже самое.

  • >>Основных решения здесь два:

    А если это случилось из-за нехватки памяти или ресурсов, то вполне вероятно, что после отката их опять не окажется в достаточном количестве. И повторная транзакция тоже провалится. И надо будет откатывать польностью всю операцию, а не только запись в m_clients и тут уже сложность отката может быть сколько угодно большой да еще и помноженная на проблемы с памятью.
    В общем, используй исключения или не используй, а обработка ошибок - это больше искусство, чем просто расстановка try…catch по коду :)

  • Alex Nekipelov

    > А если это случилось из-за нехватки памяти или ресурсов, то вполне вероятно, что после отката их опять не окажется в достаточном количестве.

    На мой взгляд, стратегия работы в данном случае должны быть простой. Если исключение возникнет в конструкторе client_info_t, то m_clients останется в корректном состоянии. Если же в конструкторе копирования, то в этом случае все плохо. Поэтому единственное исключение, которое может сгенерировать конструктор копирования - это нехватка памяти. И то желательно избегать выделения памяти в таких местах, например, используя счетчик ссылок и копирование при записи (на мой взгляд, если констурктор копирования сложный, всегда предпочтительней счетчик ссылок использовать). При этом нехватка памяти в алгоритме должна приводить к откату, обычно нет смысла пытаться что-либо исправить. Если есть возможность освободить ресурсы, то этим должен заниматься менеджер памяти, который ее и выделяет.

    По крайней мере один try/catch блок по стеку должен быть в любом случае, т.к. нехватка памяти - ситуация стандартная.

  • На мой взгляд, все-таки с исключениями нужно работать немного иначе. Если исключение произойдет в конструкторе client_info_t, то m_clients останется в прежнем (корректном состоянии). А конструктор копирования не должен бросать исключений, иначе такой класс нельзя безопасно использовать вместе с STL. Единственное допустимое исключение - нехватка памяти (в случае исключения, разумеется, полный откат алгоритма). Хотя предпочтительней использовать счетчик ссылок и копирование при записи. В этом случае конструктор копирования сводится к копированию указателя и инкрементированию счетчика, никаких опасных мест.

    Обработчик исключения bad_alloc должен быть в любом случае. Никаких дополнительных блоков try/catch для m_clients не делается и все работает корректно. Вроде бы подводных камней нет?

  • не так все просто. Во-первых, конструктор client_info_t будет вызываться несколько раз. Первый раз вызовется конструктор по умолчанию внутри m_clients[]. После этого m_clients уже изменен и это изменение нужно откатывать. Второй раз конструктор будет вызываться для client_info_t в правой части присваивания. Причем, не факт, что порядок вызова конструкторов будет именно таким. Все может быть и наоборот. Потом еще вызовется и оператор копирования. Который (в отличии от деструктора) вполне может бросать исключения. Так что для одной простой операции слишком много мест потенциального возникновения исключения.
    PS. счетчики ссылок и copy-on-write — это геморрой при многопоточном программировании.

  • Для сложных объектов всё-же конструктор копирования может и должен память выделять и даже ресурсы какие-то может захватывать. И тут могут быть исключения. Но я бы от них вообще избавился - ловил бы даже bad_alloc в конструкторе копирования и не пускал его дальше. А если состояние надо проверить - добавил бы в сам объект состояние “Ошибка”. То есть, после m_clients[name]= obj, можно просто проверить, что m_clietns[name].initialized().
    А вообще, если сделать obj указателем, то вообще никаких проблем :)

  • > То есть, после m_clients[name]= obj, можно просто проверить, что m_clietns[name].initialized().
    > А вообще, если сделать obj указателем, то вообще никаких проблем

    Какой тогда смысл в использовании C++, если писать на нем как на C?

  • >> > То есть, после m_clients[name]= obj, можно просто проверить, что m_clietns[name].initialized().
    >> > А вообще, если сделать obj указателем, то вообще никаких проблем
    >>
    >> Какой тогда смысл в использовании C++, если писать на нем как на C?

    БОльшая часть C++ - это C. Какой смысл это не использовать, если это удобнее? Имхо, проверить .initialized() гораздо проще, чем ловить исключение и обрабатывать все ситуации, когда оно могло быть брошено. Да и исключений может быть брошено множество, да и при переходе от одной версии STL к другой они могут поменяться, а .initialized() будет работать всегда, т.к. ты его контролируешь сам.
    Мой принцип - “не усложняй”. Если на C написать проще и понятнее, то почему бы не написать на С?

  • konstantin

    А вот не уверен, что использование .initialized() логически вернее…
    Т.е. ПММ такой то подход удобнее использовать, чем работать с исключениями, но вот именно при такой записи начинает сильно смахивать на то, что у нас есть проблема с 3-я состояниями объекта:
    1) объект не создан
    2) объект создан, но не инициализирован
    3) объект создан, и инициализирован - готов к работе
    И вот в пространстве между №2 и №3 начинают расти весёлые баги.
    Поправьте меня, если не к этому сводилась запись с проверкой на .initialized().

  • .initialized() - это как раз проверка, что “создан, но не инициализирован”. Ибо если не создан, то перед вызовом создастся с конструктором по умолчанию.

  • konstantin

    >>.initialized() - это как раз проверка, что “создан, но не инициализирован”. Ибо если не создан, то перед вызовом создастся с конструктором по умолчанию.

    Вот как раз в таком стиле лучше и не писать - слишком легко забыть проверить “а всё ли инициализировано успешно?”. Лучше уж писать вместо:

    m_clients[ client_name ] = client_info_t( params );
    if(m_clietns[name].initialized())


    вот так:

    if( m_clients.Add( client_name, client_info_t( params ) )
    ...

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

  • Согласен, так лучше писать. Если есть функция типа Add, которая возвращает код ошибки для операции - надо использовать ее. .initialized() нужна только в тех случаях, когда по время операции могло что-то случиться, что оставит объект в неопределенном состоянии.

Ответить

 

 

 

Вы можете использовать эти HTML тэги

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>