Я профессиональный программист на C и любитель Obj-C (OS X). Недавно я испытал желание расшириться до C ++ из-за его очень богатого синтаксиса.
До сих пор кодирование я не имел дело с исключениями. У Objective-C они есть, но политика Apple довольно строгая:
Внимание ! Вам следует зарезервировать использование исключений для программирования или непредвиденных ошибок во время выполнения, таких как доступ за пределы коллекции, попытки изменить неизменяемые объекты, отправить недопустимое сообщение и потерять соединение с оконным сервером.
С ++ предпочитает чаще использовать исключения. Например, пример википедии о RAII выдает исключение, если файл не может быть открыт. Objective-C мог бы return nil
с ошибкой, посланной out param. Примечательно, что std :: ofstream может быть установлен в любом случае.
Здесь, на программистах, я нашел несколько ответов, объявляющих об использовании исключений вместо кодов ошибок или об отсутствии исключений вообще . Первые кажутся более распространенными.
Я не нашел никого, кто делал бы объективное исследование для C ++. Мне кажется, что, поскольку указатели встречаются редко, мне придется использовать флаги внутренних ошибок, если я решу избежать исключений. Будет ли это слишком сложно, или, возможно, будет работать даже лучше, чем исключения? Сравнение обоих случаев будет лучшим ответом.
Изменить: Хотя это не совсем актуально, я, вероятно, должен уточнить, что это nil
такое. Технически это то же самое NULL
, но дело в том, что можно отправлять сообщения nil
. Таким образом, вы можете сделать что-то вроде
NSError *err = nil;
id obj = [NSFileHandle fileHandleForReadingFromURL:myurl error:&err];
[obj retain];
даже если первый звонок вернулся nil
. И как вы никогда не делаете *obj
в Obj-C, нет риска разыменования нулевого указателя.
источник
Ответы:
Я бы предложил на самом деле меньше, чем Objective-C, в некоторых отношениях, потому что стандартная библиотека C ++ обычно не создавала бы ошибок программиста, таких как доступ за пределы последовательности произвольного доступа в ее наиболее распространенной форме (
operator[]
например,), или пытаясь разыменовать неверный итератор. Язык не дает доступа к массиву вне границ, разыменования нулевого указателя или чего-либо подобного.Исключение ошибок программиста в основном из уравнения обработки исключений фактически убирает очень большую категорию ошибок, на которые часто реагируют другие языки
throwing
. C ++ имеет тенденциюassert
(что не компилируется в сборках выпуска / производства, только отладочные сборки) или просто сбивается (часто происходит сбой) в таких случаях, возможно, отчасти потому, что язык не хочет налагать затраты на такие проверки во время выполнения как было бы необходимо для обнаружения таких ошибок программиста, если программист специально не хочет оплачивать расходы путем написания кода, который выполняет такие проверки самостоятельно.Саттер даже рекомендует избегать исключений в таких случаях в стандартах кодирования C ++:
Это правило не обязательно изложено в камне. В некоторых более критических случаях может быть предпочтительнее использовать, скажем, оболочки и стандарт кодирования, который единообразно регистрирует, где происходят ошибки программиста, и
throw
при наличии ошибок программиста, таких как попытка почтить что-то недопустимое или получить доступ к нему за пределами, потому что Это может быть слишком дорого, чтобы не восстановиться в тех случаях, если у программного обеспечения есть шанс. Но в целом более распространенное использование языка склоняется к тому, чтобы не бросать вызов ошибкам программиста.Внешние исключения
Я вижу исключения, которые чаще всего поощряются в C ++ (например, согласно стандартному комитету) для «внешних исключений», как неожиданный результат в каком-то внешнем источнике вне программы. Пример не в состоянии выделить память. Другой не удается открыть критический файл, необходимый для запуска программного обеспечения. Другой не удается подключиться к необходимому серверу. Другой - пользователь, нажимающий кнопку прерывания, чтобы отменить операцию, чей путь выполнения общего случая ожидает успеха, без этого внешнего прерывания. Все эти вещи находятся вне контроля непосредственного программного обеспечения и программистов, которые его написали. Это неожиданные результаты из внешних источников, которые препятствуют успешному выполнению операции (которую в моей книге действительно следует рассматривать как неделимую транзакцию *).
операции
Я часто рекомендую рассматривать
try
блок как «транзакцию», потому что транзакции должны быть успешными в целом или терпеть неудачу в целом. Если мы пытаемся что-то сделать и это не удается на полпути, то любые побочные эффекты / мутации, внесенные в состояние программы, обычно необходимо откатить, чтобы вернуть систему в правильное состояние, как будто транзакция вообще никогда не выполнялась, точно так же, как СУБД, которая не может обработать запрос на полпути, не должна нарушать целостность базы данных. Если вы изменяете состояние программы непосредственно в указанной транзакции, то вы должны «включить» ее при обнаружении ошибки (и здесь средства защиты границ могут быть полезны с RAII).Гораздо более простая альтернатива - не изменять исходное состояние программы; Вы можете изменить его копию, а затем, если это удастся, заменить копию оригиналом (убедитесь, что своп не может сгенерировать). Если это не удается, откажитесь от копии. Это также применимо, даже если вы вообще не используете исключения для обработки ошибок. «Транзакционный» образ мыслей является ключом к правильному восстановлению, если до появления ошибки произошли мутации состояния программы. Он либо преуспевает в целом, либо проваливается как целое. На полпути ему не удается совершать мутации.
Это одна из наименее часто обсуждаемых тем, когда я вижу программистов, спрашивающих о том, как правильно выполнять обработку ошибок или исключений, однако наиболее трудно из них получить право на любое программное обеспечение, которое хочет напрямую изменять состояние программы во многих из них. его операции. Чистота и неизменность могут помочь в достижении безопасности исключений в той же мере, в какой они помогают в обеспечении безопасности потоков, поскольку возникновение мутации / внешнего побочного эффекта, которое не возникает, не нужно откатывать.
Представление
Другим руководящим фактором в том, использовать ли исключения или нет, является производительность, и я не имею в виду какой-то навязчивый, скупой, непродуктивный способ. Многие компиляторы C ++ реализуют так называемую обработку исключений с нулевой стоимостью.
Согласно тому, что я читал об этом, это делает ваши пути выполнения общего случая не требующими дополнительной нагрузки (даже не той, которая обычно сопровождает обработку и распространение кода ошибки в стиле C), в обмен на значительное отклонение затрат в сторону исключительных путей ( а это значит
throwing
сейчас дороже, чем когда-либо).«Дорогой» немного сложно измерить, но для начала, вы, вероятно, не хотите бросать миллион раз в какой-то крутой петле. Этот тип дизайна предполагает, что исключения не происходят все время слева и справа.
Non-ошибка
И этот момент производительности подводит меня к ошибкам, что удивительно нечетко, если мы посмотрим на все виды других языков. Но я бы сказал, учитывая упомянутую выше конструкцию EH с нулевой стоимостью, что вы почти наверняка не захотите
throw
в ответ на то, что ключ не найден в наборе. Потому что, возможно, это не просто ошибка (человек, который ищет ключ, возможно, построил набор и ожидал, что он ищет ключи, которые не всегда существуют), но это было бы чрезвычайно дорого в этом контексте.Например, функция пересечения множеств может захотеть перебрать два множества и найти общие для них ключи. Если
threw
вам не удастся найти ключ , вы будете зацикливаться и можете встретить исключения в половине или более итераций:Приведенный выше пример абсолютно нелеп и преувеличен, но я видел, что в производственном коде некоторые люди, пришедшие из других языков, используют исключения в C ++, что-то вроде этого, и я думаю, что это довольно практичное утверждение, что это нецелесообразное использование исключений вообще в C ++. Еще один совет, приведенный выше, заключается в том, что вы заметите, что
catch
блоку абсолютно нечего делать, и он просто написан для принудительного игнорирования любых таких исключений, и это, как правило, намек (хотя и не гарант), что исключения, вероятно, не используются должным образом в C ++.Для этих типов случаев некоторый тип возвращаемого значения, указывающего на ошибку (что-либо, начиная с возврата
false
к недопустимому итератору илиnullptr
что-то еще, имеющее смысл в контексте), обычно намного более уместен, а также часто более практичен и продуктивен, так как тип без ошибок case обычно не требует некоторого процесса раскрутки стека для достижения аналогичногоcatch
сайта.Вопросов
Прямой отказ от исключений в C ++ кажется мне крайне контрпродуктивным, если только вы не работаете в какой-то встроенной системе или в конкретном случае, который запрещает их использование (в этом случае вам также придется изо всех сил избегать всех библиотека и языковой функционал, который в противном случае
throw
хотелось бы строго использоватьnothrow
new
).Если вам абсолютно необходимо избегать исключений по какой-либо причине (например: работать через границы C API модуля, чей C API вы экспортируете), многие могут не согласиться со мной, но я бы фактически предложил использовать глобальный обработчик ошибок / статус, такой как OpenGL с
glGetError()
. Вы можете заставить его использовать локальное хранилище потока, чтобы иметь уникальный статус ошибки для каждого потока.Мое объяснение этому заключается в том, что я не привык видеть, что команды в производственных средах тщательно проверяют все возможные ошибки, к сожалению, когда возвращаются коды ошибок. Если бы они были тщательными, некоторые API C могли бы столкнуться с ошибкой практически с каждым вызовом API C, и для тщательной проверки потребуется что-то вроде:
... с почти каждой строкой кода, вызывающей API, требующий таких проверок. И все же мне не повезло работать с такими тщательными командами. Они часто игнорируют такие ошибки половину, иногда даже большую часть времени. Это самая большая привлекательность для меня исключений. Если мы обернем этот API и сделаем его единообразным
throw
при возникновении ошибки, исключение нельзя игнорировать , и, на мой взгляд, и опыт, в этом и заключается превосходство исключений.Но если исключения не могут быть использованы, то глобальное состояние ошибки для каждого потока, по крайней мере, имеет преимущество (огромное по сравнению с возвратом мне кодов ошибок), заключающееся в том, что у него может быть шанс обнаружить предыдущую ошибку чуть позже, чем когда произошло в какой-то неаккуратной кодовой базе, вместо того, чтобы просто ее пропустить и оставить нас совершенно не замечающими, что произошло Ошибка могла произойти за несколько строк до или во время предыдущего вызова функции, но при условии, что программное обеспечение еще не вышло из строя, мы могли бы начать работать в обратном направлении и выяснить, где и почему это произошло.
Я бы не сказал, что указатели встречаются редко. В C ++ 11 и далее существуют даже методы, позволяющие получить указатели на данные в контейнерах и новое
nullptr
ключевое слово. Обычно считается неразумным использовать необработанные указатели для владения / управления памятью, еслиunique_ptr
вместо этого можно использовать что-то подобное, учитывая, насколько важно быть RAII-соответствующим при наличии исключений. Но необработанные указатели, которые не владеют / не управляют памятью, не обязательно считаются настолько плохими (даже от таких людей, как Саттер и Страуструп) и иногда очень практичными в качестве способа указания на вещи (наряду с индексами, указывающими на вещи).Возможно, они не менее безопасны, чем стандартные контейнерные итераторы (по крайней мере, в выпуске, отсутствующие проверенные итераторы), которые не обнаружат, если вы попытаетесь разыменовать их после того, как они станут недействительными. Я бы сказал, что C ++ до сих пор является немного опасным языком, если только ваше конкретное использование не захочет обернуть все и скрыть даже не имеющие необработанных указателей. Исключительно важно, за исключением того, что ресурсы соответствуют RAII (который обычно предоставляется без затрат времени выполнения), но кроме этого он не обязательно пытается быть самым безопасным языком для использования во избежание затрат, которые разработчик явно не хочет в обмен на что-то другое. Рекомендуемое использование не пытается защитить вас от таких вещей, как висячие указатели и недействительные итераторы, так сказать (иначе мы бы рекомендовали использовать
shared_ptr
повсюду, против чего Страуструп категорически против). Он пытается защитить вас от неспособности правильно освободить / освободить / уничтожить / разблокировать / очистить ресурс, когда что-тоthrows
.источник
Вот в чем дело: из-за уникальной истории и гибкости C ++ вы можете найти кого-то, кто высказал бы практически любое мнение о любой функции, которая вам нравится. Однако, в целом, чем больше то, что вы делаете, выглядит как C, тем хуже идея.
Одна из причин того, что C ++ намного слабее, когда дело доходит до исключений, заключается в том, что вы не можете точно,
return nil
когда вам это нравится. Там нет такого понятия, какnil
в подавляющем большинстве случаев и типов.Но вот простой факт. Исключения делают свою работу автоматически. Когда вы генерируете исключение, RAII вступает во владение, и все обрабатывается компилятором. Когда вы используете коды ошибок, вы должны помнить, чтобы проверить их. Это по своей сути делает исключения значительно более безопасными, чем коды ошибок - они как бы проверяют сами себя. Кроме того, они более выразительны. Сгенерируйте исключение, и вы можете получить строку, сообщающую вам, в чем заключается ошибка, и даже может содержать конкретную информацию, такую как «Неверный параметр X, который имел значение Y вместо Z». Получить код ошибки 0xDEADBEEF и что именно пошло не так? Я очень надеюсь, что документация будет полной и актуальной, и даже если вы получите «Плохой параметр», он никогда не скажет вам, какойпараметр, какое значение он имел, и какое значение он должен был иметь. Если вы ловите также по ссылке, они могут быть полиморфными. Наконец, исключения могут быть выброшены из мест, где коды ошибок никогда не могут, как конструкторы. А как насчет общих алгоритмов? Как
std::for_each
собирается обрабатывать ваш код ошибки? Pro-tip: это не так.Исключения значительно превосходят коды ошибок во всех отношениях. Настоящий вопрос в исключениях против утверждений.
Вот вещь Никто не может знать заранее, какие предварительные условия есть у вашей программы для работы, какие неисправности являются необычными, какие можно заранее проверить, и какие ошибки. Как правило, это означает, что вы не можете заранее решить, должен ли данный сбой быть утверждением или исключением, не зная логику программы. Кроме того, операция, которая может продолжаться при сбое одной из ее подопераций, является исключением , а не правилом.
На мой взгляд, исключения должны быть пойманы. Не обязательно сразу, но в конце концов. Исключением является проблема, от которой вы ожидаете, что в какой-то момент программа сможет восстановиться. Но рассматриваемая операция никогда не может оправиться от проблемы, которая требует исключения.
Ошибки подтверждения всегда являются фатальными, неустранимыми ошибками. Повреждение памяти, такого рода вещи.
Итак, когда вы не можете открыть файл, это утверждение или исключение? Что ж, в универсальной библиотеке существует множество сценариев, в которых ошибка может быть обработана, например, при загрузке файла конфигурации, вы можете просто использовать вместо этого встроенный стандарт, так что я бы сказал исключение.
В качестве сноски я хотел бы упомянуть, что есть какая-то вещь «Null Object Pattern». Эта картина ужасна . Через десять лет это будет следующий синглтон. Количество случаев, в которых вы можете создать подходящий нулевой объект, минимально .
источник
Исключения были придуманы по причине, которая не должна выглядеть так:
Вместо этого вы получите что-то, похожее на следующее, с одним обработчиком исключений, расположенным выше
main
или иным образом удобно расположенным:Некоторые люди заявляют о преимуществах производительности при использовании подхода без исключений, но, по моему мнению, проблемы с читабельностью перевешивают незначительный прирост производительности, особенно если вы включаете время выполнения для всех
if (!success)
шаблонов, которые вы должны разбрызгивать повсюду, или риск более сложной отладки ошибок сегмента, если вы не t включить его, и, учитывая вероятность возникновения исключений, относительно редки.Я не знаю, почему Apple не рекомендует использовать исключения. Если они пытаются избежать распространения необработанных исключений, все, что вы на самом деле делаете, это люди, использующие нулевые указатели для указания исключений, поэтому ошибка программиста приводит к исключению с нулевым указателем вместо гораздо более полезного исключения из файла, не найденного, или чего-то еще.
источник
В посте, на который вы ссылаетесь (исключения за исключением кодов ошибок), я думаю, что происходит немного другое обсуждение. Кажется, возникает вопрос, есть ли у вас глобальный список кодов ошибок #define, возможно, с такими именами, как ERR001_FILE_NOT_WRITEABLE (или сокращенно, если вам не повезло). И я думаю, что главное в этом потоке заключается в том, что если вы программируете на полиморфном языке, используя экземпляры объектов, такая процедурная конструкция не является необходимой. Исключения могут выражать, какая ошибка происходит просто в силу их типа, и они могут инкапсулировать информацию, например, какое сообщение распечатать (и другие вещи, в том числе). Итак, я бы воспринял этот разговор как вопрос о том, следует ли программировать процедурно на объектно-ориентированном языке или нет.
Но разговор о том, когда и сколько полагаться на исключения для обработки ситуаций, возникающих в коде, отличается. Когда исключения генерируются и перехватываются, вы вводите совершенно другую парадигму потока управления из традиционного стека вызовов. Исключение - это, по сути, оператор goto, который вырывает вас из стека вызовов и отправляет в какое-то неопределенное место (где ваш клиентский код решает обработать ваше исключение). Это делает логику исключений очень трудной для рассуждения .
И так, есть мнение, выраженное Джерико, что их следует минимизировать. То есть, допустим, вы читаете файл, который может существовать или не существовать на диске. Джерико и Apple, а также те, кто думает так же, как и они (включая меня), утверждают, что выбрасывать исключение нецелесообразно - отсутствие этого файла не является исключительным , как ожидается. Исключения не должны заменять нормальный поток управления. Так же просто, чтобы ваш метод ReadFile () возвращал, скажем, логическое значение, и чтобы клиентский код видел из возвращаемого значения false, что файл не был найден. Затем клиентский код может сообщить пользователю, что файл не был найден, или спокойно обработать этот случай, или все, что он хочет сделать.
Когда вы бросаете исключение, вы обременяете своих клиентов. Вы даете им код, который иногда вырывает их из обычного стека вызовов и вынуждает их писать дополнительный код, чтобы предотвратить сбой их приложения. Такое мощное и неприятное бремя должно быть наложено на них только в случае крайней необходимости во время выполнения или в интересах философии «провалить рано». Таким образом, в первом случае вы можете генерировать исключения, если целью вашего приложения является мониторинг какой-либо сетевой операции, а кто-то отключает сетевой кабель (вы сигнализируете о катастрофическом сбое). В последнем случае вы можете выдать исключение, если ваш метод ожидает ненулевой параметр, и вы получили нулевое значение (вы сигнализируете, что вы используете ненадлежащим образом).
Что касается того, что нужно делать вместо исключений в объектно-ориентированном мире, у вас есть варианты, выходящие за рамки процедурной кодовой ошибки. Сразу вспоминается, что вы можете создавать объекты с именем OperationResult или что-то подобное. Метод может вернуть OperationResult, и этот объект может иметь информацию о том, успешно ли выполнена операция, и любую другую информацию, которую вы хотите, чтобы она имела (например, сообщение о состоянии, но также, что более тонко, он может инкапсулировать стратегии восстановления после ошибок). ). Возврат этого вместо выдачи исключения учитывает полиморфизм, сохраняет поток управления и значительно упрощает отладку.
источник
В « Чистом коде» есть прекрасная пара глав, в которых говорится об этой самой теме - минус точка зрения Apple.
Основная идея заключается в том, что вы никогда не возвращаете nil из своих функций, потому что это:
Другая сопровождающая идея заключается в том, что вы используете исключения, потому что это помогает еще больше уменьшить беспорядок в вашем коде, и вместо того, чтобы создавать серию кодов ошибок, которые в конечном итоге становятся трудными для отслеживания управления и обслуживания (особенно для нескольких модулей и платформ) и Ваши клиенты испытывают трудности с расшифровкой, вместо этого вы создаете осмысленные сообщения, которые идентифицируют точную природу проблемы, упрощают отладку и могут уменьшить ваш код обработки ошибок до чего-то простого, чем базовое
try..catch
утверждение.В те дни, когда я разрабатывал COM, у меня был проект, в котором каждый сбой вызывал исключение, и каждое исключение должно было использовать уникальный идентификатор кода ошибки, основанный на расширении стандартных окон COM. Это было воздержание от более раннего проекта, в котором ранее использовались коды ошибок, но компания хотела пойти на все объектно-ориентированные и перейти на COM-фургон, не изменяя способ, которым они делали вещи слишком много. Им потребовалось всего 4 месяца, чтобы исчерпать номера кодов ошибок, и мне пришлось реорганизовать большие участки кода, чтобы вместо них использовать исключения. Лучше сэкономить время и просто разумно использовать исключения.
источник