Является ли использование assert () в C ++ плохой практикой?

94

Я обычно добавляю множество утверждений в свой код на C ++, чтобы упростить отладку, не влияя на производительность сборок выпуска. Теперь assertэто чистый C макро разработан без механизмов C ++ в виду.

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

Проблема заключается в том, что assertи abortкак завершить программу сразу без вызова деструкторов, поэтому пропуск очистки, в то время как бросать исключение вручную добавляет ненужные затраты времени выполнения. Один из способов обойти это - создать собственный макрос утверждения SAFE_ASSERT, который работает так же, как аналог C, но выдает исключение при ошибке.

Я могу придумать три мнения по этой проблеме:

  • Придерживайтесь утверждения С. Поскольку программа завершается немедленно, не имеет значения, правильно ли развернуты изменения. Кроме того, использование #defines в C ++ так же плохо.
  • Бросить исключение и поймать его в main () . Разрешение коду пропускать деструкторы в любом состоянии программы является плохой практикой, и ее следует избегать любой ценой, как и вызовов terminate (). Если выбрасываются исключения, их нужно перехватывать.
  • Вызвать исключение и позволить ему завершить программу. Исключение, завершающее программу, - это нормально, и поэтому NDEBUGэтого никогда не произойдет в сборке выпуска. В захвате нет необходимости, и он раскрывает детали реализации внутреннего кода main().

Есть ли однозначный ответ на эту проблему? Любая профессиональная рекомендация?

Отредактировано: пропуск деструкторов, конечно, не является неопределенным поведением.

Фабиан Кнорр
источник
22
Нет, действительно, logic_errorэто логическая ошибка. Ошибка в логике программы называется ошибкой. Вы не решаете ошибки, выбрасывая исключения.
Р. Мартиньо Фернандес
4
Утверждения, исключения, коды ошибок. У каждого из них есть совершенно разные варианты использования, и вы не должны использовать один там, где нужен другой.
Kerrek SB
5
Убедитесь, что вы используете static_assertтам, где это уместно, если оно у вас есть.
Flexo
4
@trion Не понимаю, как это помогает. Вы бы бросили std::bug?
R. Martinho Fernandes
3
@trion: Не делай этого. Исключения не для отладки. Кто-то может уловить исключение. При звонке не нужно беспокоиться о UB std::abort(); он просто вызовет сигнал, который приведет к завершению процесса.
Kerrek SB

Ответы:

74

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

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

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


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

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

bames53
источник
1
Я не совсем уверен, было ли это заявлено конкретно в ответе, поэтому я скажу здесь: вы не должны использовать утверждение для чего-либо, связанного с вводом пользователя, которое не может быть определено во время написания кода. Если пользователь переходит 3вместо 1вашего кода, в общем случае это не должно вызывать утверждения. Утверждения - это только ошибка программиста, а не ошибка пользователя библиотеки или приложения.
SS Anne
101
  • Утверждения предназначены для отладки . Пользователь вашего отправленного кода никогда не должен их видеть. Если утверждение выполнено, ваш код необходимо исправить.

    CWE-617: достижимое утверждение

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

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

Например, если сервер обрабатывает несколько одновременных подключений, а assert () возникает в одном подключении, что приводит к разрыву всех остальных подключений, это достижимое утверждение, которое приводит к отказу в обслуживании.

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

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

Керрек С.Б.
источник
1
пришел искать это повторное утверждение; любая форма утверждения, передаваемая в производственный код, указывает на плохой дизайн и контроль качества. Точка, где вызывается утверждение, - это место, где должна быть корректная обработка состояния ошибки. (Я никогда не использую assert). Что касается исключений, единственный известный мне вариант использования - это когда ctor может выйти из строя, все остальные предназначены для нормальной обработки ошибок.
slashmais
5
@slashmais: Настроение похвально, но если вы не поставляете идеальный код без ошибок, я считаю утверждение (даже такое, которое приводит к сбою пользователя) предпочтительнее неопределенного поведения. Ошибки случаются в сложных системах, и с утверждением у вас есть способ увидеть и диагностировать их там, где они происходят.
Керрек С.Б.
@KerrekSB Я бы предпочел использовать исключение вместо утверждения. По крайней мере, у кода есть шанс отбросить неудачную ветвь и сделать что-то еще полезное. По крайней мере, если вы используете RAII, все ваши буферы для открытия файлов будут очищены должным образом.
daemonspring
14

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

ногард
источник
У меня похожий подход. Я использую утверждения для логики, которые, вероятно, должны быть правильными локально (например, инварианты цикла). Исключения составляют случаи, когда логическая ошибка была навязана коду нелокальной (внешней) ситуацией.
spraff
Если утверждение не выполняется, это означает, что логика части программы нарушена. Неудачное утверждение не обязательно означает, что ничего нельзя сделать. Неисправный плагин, вероятно, не должен прерывать работу всего текстового процессора.
daemonspring
13

Отсутствие деструкторов из-за alling abort () не является неопределенным поведением!

Если бы это было так, то это тоже было бы неопределенное поведение std::terminate(), поэтому какой смысл в его предоставлении?

assert() столь же полезен в C ++, как и в C. Утверждения предназначены не для обработки ошибок, а для немедленного прерывания программы.

Джонатан Уэйкли
источник
1
Я бы сказал abort(), для немедленного прерывания программы. Вы правы, что утверждения не предназначены для обработки ошибок, но assert все же пытается обработать ошибку путем прерывания. Разве вам не следует вместо этого создать исключение и позволить вызывающей стороне обработать ошибку, если это возможно? В конце концов, вызывающий может лучше определить, делает ли сбой одной функции нецелесообразным делать что-либо еще. Возможно, вызывающий пытается выполнить три несвязанных друг с другом дела, но может завершить два других задания и просто отказаться от этого.
daemonspring
И assertопределен для вызова abort(когда условие ложно). Что касается исключения исключений, нет, это не всегда уместно. Некоторые вещи не могут быть выполнены вызывающим абонентом. Вызывающий не может определить, можно ли исправить логическую ошибку в функции сторонней библиотеки или исправить поврежденные данные.
Джонатан
6

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

Я бы сгруппировал их в 2 категории:

  • Грехи разработчика (например, функция вероятности, возвращающая отрицательные значения):

вероятность с плавающей точкой () {возврат -1,0; }

assert (вероятность ()> = 0,0)

  • Машина сломана (например, машина, на которой запущена ваша программа, очень неправильная):

int x = 1;

assert (x> 0);

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

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

FranMowinckel
источник
1
assert (вероятность ()> = 0.0)
Эллиотт