С ++ предпочитает чаще использовать исключения.
Я бы предложил на самом деле меньше, чем 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
.