Исключения как утверждения или как ошибки?

10

Я профессиональный программист на 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 ++ вам нужно скорее оправдание, чтобы не использовать их. По реакции здесь.
Гангнус
2
Возможно, но до сих пор никто не объяснял, почему они лучше (кроме по сравнению с кодами ошибок). Мне не нравится концепция использования исключений для вещей, которые не являются исключительными, но они более инстинктивны, чем основаны на фактах.
Пер Йоханссон
«Мне не нравится ... использовать исключения для вещей, которые не являются исключительными», - согласился.
Гангнус

Ответы:

1

С ++ предпочитает чаще использовать исключения.

Я бы предложил на самом деле меньше, чем Objective-C, в некоторых отношениях, потому что стандартная библиотека C ++ обычно не создавала бы ошибок программиста, таких как доступ за пределы последовательности произвольного доступа в ее наиболее распространенной форме ( operator[]например,), или пытаясь разыменовать неверный итератор. Язык не дает доступа к массиву вне границ, разыменования нулевого указателя или чего-либо подобного.

Исключение ошибок программиста в основном из уравнения обработки исключений фактически убирает очень большую категорию ошибок, на которые часто реагируют другие языки throwing. C ++ имеет тенденцию assert(что не компилируется в сборках выпуска / производства, только отладочные сборки) или просто сбивается (часто происходит сбой) в таких случаях, возможно, отчасти потому, что язык не хочет налагать затраты на такие проверки во время выполнения как было бы необходимо для обнаружения таких ошибок программиста, если программист специально не хочет оплачивать расходы путем написания кода, который выполняет такие проверки самостоятельно.

Саттер даже рекомендует избегать исключений в таких случаях в стандартах кодирования C ++:

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

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

Внешние исключения

Я вижу исключения, которые чаще всего поощряются в C ++ (например, согласно стандартному комитету) для «внешних исключений», как неожиданный результат в каком-то внешнем источнике вне программы. Пример не в состоянии выделить память. Другой не удается открыть критический файл, необходимый для запуска программного обеспечения. Другой не удается подключиться к необходимому серверу. Другой - пользователь, нажимающий кнопку прерывания, чтобы отменить операцию, чей путь выполнения общего случая ожидает успеха, без этого внешнего прерывания. Все эти вещи находятся вне контроля непосредственного программного обеспечения и программистов, которые его написали. Это неожиданные результаты из внешних источников, которые препятствуют успешному выполнению операции (которую в моей книге действительно следует рассматривать как неделимую транзакцию *).

операции

Я часто рекомендую рассматривать tryблок как «транзакцию», потому что транзакции должны быть успешными в целом или терпеть неудачу в целом. Если мы пытаемся что-то сделать и это не удается на полпути, то любые побочные эффекты / мутации, внесенные в состояние программы, обычно необходимо откатить, чтобы вернуть систему в правильное состояние, как будто транзакция вообще никогда не выполнялась, точно так же, как СУБД, которая не может обработать запрос на полпути, не должна нарушать целостность базы данных. Если вы изменяете состояние программы непосредственно в указанной транзакции, то вы должны «включить» ее при обнаружении ошибки (и здесь средства защиты границ могут быть полезны с RAII).

Гораздо более простая альтернатива - не изменять исходное состояние программы; Вы можете изменить его копию, а затем, если это удастся, заменить копию оригиналом (убедитесь, что своп не может сгенерировать). Если это не удается, откажитесь от копии. Это также применимо, даже если вы вообще не используете исключения для обработки ошибок. «Транзакционный» образ мыслей является ключом к правильному восстановлению, если до появления ошибки произошли мутации состояния программы. Он либо преуспевает в целом, либо проваливается как целое. На полпути ему не удается совершать мутации.

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

Представление

Другим руководящим фактором в том, использовать ли исключения или нет, является производительность, и я не имею в виду какой-то навязчивый, скупой, непродуктивный способ. Многие компиляторы C ++ реализуют так называемую обработку исключений с нулевой стоимостью.

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

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

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

Non-ошибка

И этот момент производительности подводит меня к ошибкам, что удивительно нечетко, если мы посмотрим на все виды других языков. Но я бы сказал, учитывая упомянутую выше конструкцию EH с нулевой стоимостью, что вы почти наверняка не захотите throwв ответ на то, что ключ не найден в наборе. Потому что, возможно, это не просто ошибка (человек, который ищет ключ, возможно, построил набор и ожидал, что он ищет ключи, которые не всегда существуют), но это было бы чрезвычайно дорого в этом контексте.

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

Set<int> set_intersection(const Set<int>& a, const Set<int>& b)
{
     Set<int> intersection;
     for (int key: a)
     {
          try
          {
              b.find(key);
              intersection.insert(other_key);
          }
          catch (const KeyNotFoundException&)
          {
              // Do nothing.
          }
     }
     return intersection;
}

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

Для этих типов случаев некоторый тип возвращаемого значения, указывающего на ошибку (что-либо, начиная с возврата falseк недопустимому итератору или nullptrчто-то еще, имеющее смысл в контексте), обычно намного более уместен, а также часто более практичен и продуктивен, так как тип без ошибок case обычно не требует некоторого процесса раскрутки стека для достижения аналогичного catchсайта.

Вопросов

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

Прямой отказ от исключений в C ++ кажется мне крайне контрпродуктивным, если только вы не работаете в какой-то встроенной системе или в конкретном случае, который запрещает их использование (в этом случае вам также придется изо всех сил избегать всех библиотека и языковой функционал, который в противном случае throwхотелось бы строго использовать nothrow new).

Если вам абсолютно необходимо избегать исключений по какой-либо причине (например: работать через границы C API модуля, чей C API вы экспортируете), многие могут не согласиться со мной, но я бы фактически предложил использовать глобальный обработчик ошибок / статус, такой как OpenGL с glGetError(). Вы можете заставить его использовать локальное хранилище потока, чтобы иметь уникальный статус ошибки для каждого потока.

Мое объяснение этому заключается в том, что я не привык видеть, что команды в производственных средах тщательно проверяют все возможные ошибки, к сожалению, когда возвращаются коды ошибок. Если бы они были тщательными, некоторые API C могли бы столкнуться с ошибкой практически с каждым вызовом API C, и для тщательной проверки потребуется что-то вроде:

if ((err = ApiCall(...)) != success)
{
     // Handle error
}

... с почти каждой строкой кода, вызывающей API, требующий таких проверок. И все же мне не повезло работать с такими тщательными командами. Они часто игнорируют такие ошибки половину, иногда даже большую часть времени. Это самая большая привлекательность для меня исключений. Если мы обернем этот API и сделаем его единообразным throwпри возникновении ошибки, исключение нельзя игнорировать , и, на мой взгляд, и опыт, в этом и заключается превосходство исключений.

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

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

Я бы не сказал, что указатели встречаются редко. В C ++ 11 и далее существуют даже методы, позволяющие получить указатели на данные в контейнерах и новое nullptrключевое слово. Обычно считается неразумным использовать необработанные указатели для владения / управления памятью, если unique_ptrвместо этого можно использовать что-то подобное, учитывая, насколько важно быть RAII-соответствующим при наличии исключений. Но необработанные указатели, которые не владеют / не управляют памятью, не обязательно считаются настолько плохими (даже от таких людей, как Саттер и Страуструп) и иногда очень практичными в качестве способа указания на вещи (наряду с индексами, указывающими на вещи).

Возможно, они не менее безопасны, чем стандартные контейнерные итераторы (по крайней мере, в выпуске, отсутствующие проверенные итераторы), которые не обнаружат, если вы попытаетесь разыменовать их после того, как они станут недействительными. Я бы сказал, что C ++ до сих пор является немного опасным языком, если только ваше конкретное использование не захочет обернуть все и скрыть даже не имеющие необработанных указателей. Исключительно важно, за исключением того, что ресурсы соответствуют RAII (который обычно предоставляется без затрат времени выполнения), но кроме этого он не обязательно пытается быть самым безопасным языком для использования во избежание затрат, которые разработчик явно не хочет в обмен на что-то другое. Рекомендуемое использование не пытается защитить вас от таких вещей, как висячие указатели и недействительные итераторы, так сказать (иначе мы бы рекомендовали использоватьshared_ptrповсюду, против чего Страуструп категорически против). Он пытается защитить вас от неспособности правильно освободить / освободить / уничтожить / разблокировать / очистить ресурс, когда что-то throws.

Энергия Дракона
источник
14

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

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

Но вот простой факт. Исключения делают свою работу автоматически. Когда вы генерируете исключение, RAII вступает во владение, и все обрабатывается компилятором. Когда вы используете коды ошибок, вы должны помнить, чтобы проверить их. Это по своей сути делает исключения значительно более безопасными, чем коды ошибок - они как бы проверяют сами себя. Кроме того, они более выразительны. Сгенерируйте исключение, и вы можете получить строку, сообщающую вам, в чем заключается ошибка, и даже может содержать конкретную информацию, такую ​​как «Неверный параметр X, который имел значение Y вместо Z». Получить код ошибки 0xDEADBEEF и что именно пошло не так? Я очень надеюсь, что документация будет полной и актуальной, и даже если вы получите «Плохой параметр», он никогда не скажет вам, какойпараметр, какое значение он имел, и какое значение он должен был иметь. Если вы ловите также по ссылке, они могут быть полиморфными. Наконец, исключения могут быть выброшены из мест, где коды ошибок никогда не могут, как конструкторы. А как насчет общих алгоритмов? Как std::for_eachсобирается обрабатывать ваш код ошибки? Pro-tip: это не так.

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

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

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

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

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

В качестве сноски я хотел бы упомянуть, что есть какая-то вещь «Null Object Pattern». Эта картина ужасна . Через десять лет это будет следующий синглтон. Количество случаев, в которых вы можете создать подходящий нулевой объект, минимально .

DeadMG
источник
Альтернатива не обязательно простая инт. Я бы с большей вероятностью использовал что-то похожее на NSError, к которому я привык из Obj-C.
За Йоханссон
Он сравнивает не с С, а с Целью С. Это большая разница. И его коды ошибок объясняют строки. Все, что вы говорите правильно, только не отвечает на этот вопрос как-то. Без обид.
Гангнус
Любой, кто использует Objective-C, конечно, скажет вам, что вы абсолютно, совершенно неправы.
gnasher729
5

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

bool success = function1(&result1, &err);
if (!success) {
    error_handler(err);
    return;
}

success = function2(&result2, &err);
if (!success) {
    error_handler(err);
    return;
}

Вместо этого вы получите что-то, похожее на следующее, с одним обработчиком исключений, расположенным выше mainили иным образом удобно расположенным:

result1 = function1();
result2 = function2();

Некоторые люди заявляют о преимуществах производительности при использовании подхода без исключений, но, по моему мнению, проблемы с читабельностью перевешивают незначительный прирост производительности, особенно если вы включаете время выполнения для всех if (!success)шаблонов, которые вы должны разбрызгивать повсюду, или риск более сложной отладки ошибок сегмента, если вы не t включить его, и, учитывая вероятность возникновения исключений, относительно редки.

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

Карл Билефельдт
источник
1
Это имело бы больше смысла, если бы код без исключений действительно выглядел так, но обычно это не так. Этот шаблон появляется, когда error_handler никогда не возвращается, но редко в противном случае.
Пер Йоханссон
1

В посте, на который вы ссылаетесь (исключения за исключением кодов ошибок), я думаю, что происходит немного другое обсуждение. Кажется, возникает вопрос, есть ли у вас глобальный список кодов ошибок #define, возможно, с такими именами, как ERR001_FILE_NOT_WRITEABLE (или сокращенно, если вам не повезло). И я думаю, что главное в этом потоке заключается в том, что если вы программируете на полиморфном языке, используя экземпляры объектов, такая процедурная конструкция не является необходимой. Исключения могут выражать, какая ошибка происходит просто в силу их типа, и они могут инкапсулировать информацию, например, какое сообщение распечатать (и другие вещи, в том числе). Итак, я бы воспринял этот разговор как вопрос о том, следует ли программировать процедурно на объектно-ориентированном языке или нет.

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

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

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

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

Эрик Дитрих
источник
Я согласен с вами, но причина, по которой большинство, похоже, не является, заставила меня задать вопрос.
Пер Йоханссон
0

В « Чистом коде» есть прекрасная пара глав, в которых говорится об этой самой теме - минус точка зрения Apple.

Основная идея заключается в том, что вы никогда не возвращаете nil из своих функций, потому что это:

  • Добавляет много дублированного кода
  • Запутывает ваш код - то есть: становится труднее читать
  • Часто может привести к ошибкам нулевого указателя - или к нарушениям доступа в зависимости от вашей "религии" программирования

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

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

S.Robins
источник
Спасибо, но я не собираюсь возвращать NULL. В любом случае, это кажется невозможным перед лицом конструкторов. Как я уже говорил, мне, вероятно, придется использовать флаг ошибки в объекте.
За Йоханссон