Try-catch или ifs для обработки ошибок в C ++

30

Широко ли используются исключения в дизайне игрового движка или предпочтительнее использовать операторы if? Например с исключениями:

try {
    m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
    m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
    m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
    m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
    m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
    m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
} catch ... {
    // some info about error
}

и с ifs:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
if( m_fpsTextId == -1 ) {
    // show error
}
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
if( m_cpuTextId == -1 ) {
    // show error
}
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
if( m_frameTimeTextId == -1 ) {
    // show error
}
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
if( m_mouseCoordTextId == -1 ) {
    // show error
}
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
if( m_cameraPosTextId == -1 ) {
    // show error
}
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );
if( m_cameraRotTextId == -1 ) {
    // show error
}

Я слышал, что исключения немного медленнее, чем ifs, и я не должен смешивать исключения с методом if-check. Однако с исключениями у меня более читаемый код, чем с тоннами if после каждого метода initialize () или чего-то подобного, хотя они, на мой взгляд, иногда слишком тяжелы для одного метода. Они подходят для разработки игр или лучше придерживаться простых ifs?

тоби
источник
Многие утверждают, что Boost вреден для разработки игр, но я рекомендую вам взглянуть на ["Boost.optional"] ( boost.org/doc/libs/1_52_0/libs/optional/doc/html/index.html ). ваш пример ifs немного лучше
ManicQin
Есть хороший разговор об обработке ошибок Андрея Александреску, я использовал аналогичный подход к его без исключения.
Тельвин

Ответы:

48

Краткий ответ: прочитайте Обработка чувствительных ошибок 1 , Обработка понятных ошибок 2 и Обработка чувствительных ошибок 3 от Niklas Frykholm. На самом деле, прочитайте все статьи в этом блоге, пока вы на нем. Не скажу, что я согласен со всем, но в основном это золото.

Не используйте исключения. Есть множество причин. Я перечислю основные.

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

Исключения делают код гораздо более хрупким. Есть печально известная графика (которую я, к сожалению, не могу найти прямо сейчас), которая в основном просто показывает на диаграмме сложность написания кода, исключающего исключение, по сравнению с кодом, небезопасным для исключения, а первый представляет собой значительно большую полосу. Просто есть много маленьких ошибок с исключениями и множество способов написания кода, который выглядит безопасным, но на самом деле это не так. Даже весь комитет по C ++ 11 отошел от этого и забыл добавить важные вспомогательные функции для правильного использования std :: unique_ptr, и даже с этими вспомогательными функциями требуется больше набирать их, чем нет, и большинство программистов победили ' даже не понимаю, что не так, если они этого не делают.

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

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

Что касается кода, подобного вашему примеру, у вас есть несколько других способов решить проблему. Один из наиболее надежных - хотя и не обязательно самый идеальный в вашем простом примере - склоняется к типам монадических ошибок. То есть createText () может возвращать пользовательский тип дескриптора, а не целое число. Этот тип дескриптора имеет средства доступа для обновления или управления текстом. Если дескриптор переводится в состояние ошибки (из-за сбоя createText ()), дальнейшие вызовы дескриптора просто молча завершаются неудачей. Вы также можете запросить дескриптор, чтобы узнать, не ошиблась ли она, и если да, то какая была последняя ошибка. Этот подход имеет больше накладных расходов, чем другие варианты, но он довольно солидный. Используйте его в тех случаях, когда вам нужно выполнить длинную строку операций в некотором контексте, когда любая отдельная операция может потерпеть неудачу в производстве, но где вы не можете / не можете / выиграли '

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

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

Лучший вариант (уже представленный в серии связанных статей выше) для вызовов, которые вы не ожидаете, что произойдет сбой в любой разумной среде - например, простое действие по созданию текстовых BLOB-объектов для системы статистики - просто иметь функцию этот сбой (createText в вашем примере) прерывается. Вы можете быть разумно уверены, что createText () не потерпит неудачу в производственном процессе, если что-то не будет полностью завершено (например, пользователь удалил файлы данных шрифта или по какой-то причине имеет только 256 МБ памяти и т. Д.). Во многих из этих случаев нет даже ничего хорошего, когда сбой случается. Недостаточно памяти? Возможно, вы даже не сможете сделать выделение, необходимое для создания красивой панели с графическим интерфейсом, чтобы показать пользователю ошибку OOM. Отсутствующие шрифты? Затрудняет отображение ошибок для пользователя. Что бы ни случилось,

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

Я бы даже не сказал то же самое для многих серверных приложений, где доступность критична, а сторожевого мониторинга недостаточно, но это сильно отличается от разработки игрового клиента . Я также настоятельно рекомендую вам избегать использования C / C ++, поскольку средства обработки исключений в других языках обычно не похожи на C ++, поскольку они являются управляемыми средами и не имеют всех проблем безопасности исключений, которые есть в C ++. Любые проблемы с производительностью также уменьшаются, поскольку серверы, как правило, ориентированы на параллельность и пропускную способность, а не на минимальные задержки, как игровые клиенты. Даже серверы экшн-игр для шутеров и тому подобное могут функционировать довольно хорошо, например, когда они написаны на C #, поскольку они редко выдвигают аппаратное обеспечение до предела, как это обычно делают клиенты FPS.

Шон Миддледич
источник
1
+1. Кроме того, warn_unused_resultв некоторых компиляторах есть возможность добавить атрибут like функции, который позволяет отлавливать необработанный код ошибки.
Мацей Пехотка
Пришел сюда, увидев C ++ в заголовке, и был абсолютно уверен, что обработки монадических ошибок уже не будет. Хорошая вещь!
Адам
А как насчет мест, где производительность не критична, и вы в основном стремитесь к быстрой реализации? например, когда вы реализуете игровую логику?
Ali1S232
также как насчет идеи использования обработки исключений только для целей отладки? например, когда вы выпускаете игру, вы ожидаете, что все пройдет гладко и без проблем. поэтому вы используете исключения для поиска и исправления ошибок во время разработки, а затем в режиме выпуска удалите все эти исключения.
Ali1S232
1
@Gajoo: для игровой логики исключения просто усложняют соблюдение логики (поскольку это усложняет весь код). По моему опыту, даже в Pythnn и C # исключения редки для игровой логики. для отладки hard assert обычно более удобен. Сломайте и остановите в тот момент, когда что-то пойдет не так, не после того, как вымотаете большие части стека и потеряете все виды контекстной информации из-за семантики создания исключений. Для логики отладки вы хотите создавать инструменты и информационные / редактируемые графические интерфейсы, чтобы дизайнеры могли проверять и настраивать их.
Шон Миддледич
13

Старая мудрость самого Дональда Кнута такова:

«Мы должны забыть о небольшой неэффективности, скажем, в 97% случаев: преждевременная оптимизация - корень всего зла».

Распечатайте это как большой плакат и повесьте его везде, где вы занимаетесь серьезным программированием.

Так что даже если try / catch немного медленнее, я бы использовал его по умолчанию:

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

  • Корректность по производительности. Делать вещи if / else правильно не тривиально. В вашем примере это сделано неправильно, потому что может отображаться не одна ошибка, а множественная. Вы должны использовать каскадный if / then / else.

  • Легко использовать последовательно: Try / catch охватывает стиль, в котором вам не нужно проверять ошибки в каждой строке. С другой стороны: один пропущен, если / else, и ваш код может привести к хаосу. Конечно, только в невоспроизводимых обстоятельствах.

Итак, последний пункт:

Я слышал, что исключения немного медленнее, чем если бы

Я слышал об этом около 15 лет назад, не слышал об этом ни из каких достоверных источников в последнее время. Компиляторы могли улучшиться или что угодно.

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

AH
источник
22
Я буду -1 это до бесконечности. Я не могу понять вред, который эта цитата Кнута нанесла мозгу разработчика. Учитывая, использовать ли исключения не является преждевременной оптимизацией, это - проектное решение, важное проектное решение, которое имеет последствия, выходящие далеко за пределы производительности.
Сэм Хоцевар
3
@SamHocevar: Я сожалею, я не понимаю вашу точку зрения: Вопрос это о возможной потере производительности при использовании исключений. Моя точка зрения: не думайте об этом, хит не так уж плох (если таковой имеется), и другие вещи гораздо важнее. Здесь вы, кажется, согласны, добавив «дизайнерское решение» в мой список. ХОРОШО. С другой стороны, вы говорите, что цитата Кнута плохая, подразумевая, что преждевременная оптимизация не является плохой. Но это именно то, что здесь произошло. IMO: Q не думает об архитектуре, дизайне или других алгоритмах, а только об исключениях и их влиянии на производительность.
AH
2
Я бы не согласился с ясностью в C ++. В C ++ есть непроверенные исключения, в то время как некоторые компиляторы реализуют проверенные возвращаемые значения. В результате у вас есть код, который выглядит яснее, но может иметь скрытую утечку памяти или даже поместить код в неопределенное состояние. Также сторонний код может быть небезопасным. (Ситуация отличается в Java / C #, который проверил исключения, GC, ...). По решению проекта - если они не пересекают точки API, рефакторинг от / к каждому стилю может быть сделан полуавтоматически с помощью однострочников Perl.
Мацей Пехотка
1
@SamHocevar: « Вопрос в том, что предпочтительнее, а не что быстрее». Перечитайте последний абзац вопроса. Только причина , он даже подумывает о том, не используя исключения, потому что он думает , что они могут быть медленнее. Теперь, если вы считаете, что существуют другие проблемы, которые ОП не принял во внимание, не стесняйтесь размещать их или высказывать свое мнение тем, кто это сделал. Но ОП очень четко ориентирован на выполнение исключений.
Николь Болас
3
@AH - если вы собираетесь процитировать Кнута, не забывайте об остальном (и ИМО самой важной части): « Однако мы не должны упускать наши возможности в эти критические 3%. Хороший программист не будет Утешенный такими доводами, он будет мудр, чтобы внимательно посмотреть на критический код, но только после того, как этот код будет идентифицирован ». Слишком часто можно рассматривать это как оправдание, чтобы вообще не оптимизировать или пытаться оправдать медленный код в тех случаях, когда производительность не является оптимизацией, а фактически является фундаментальным требованием.
Максимус Минимус
10

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

Я считаю, что ваш код может выглядеть так:

m_fpsTextId = m_statistics->createText( "FPS: 0", 16, 20, 20, 1.0f, 1.0f, 1.0f );
m_cpuTextId = m_statistics->createText( "CPU: 0%", 16, 20, 40, 1.0f, 1.0f, 1.0f );
m_frameTimeTextId = m_statistics->createText( "Frame time: 0", 20, 20, 60, 1.0f, 1.0f, 1.0f );
m_mouseCoordTextId = m_statistics->createText( "Mouse: (0, 0)", 20, 20, 80, 1.0f, 1.0f, 1.0f );
m_cameraPosTextId = m_statistics->createText( "Camera pos: (0, 0, 0)", 40, 20, 100, 1.0f, 1.0f, 1.0f );
m_cameraRotTextId = m_statistics->createText( "Camera rot: (0, 0, 0)", 40, 20, 120, 1.0f, 1.0f, 1.0f );

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

Сэм Хоцевар
источник