Я большой поклонник написания assert
проверок в коде C ++ как способа отлавливать случаи во время разработки, которые не могут произойти, но происходят из-за логических ошибок в моей программе. Это хорошая практика в целом.
Тем не менее, я заметил, что некоторые функции, которые я пишу (которые являются частью сложного класса), имеют более 5 утверждений, что может быть плохой практикой программирования с точки зрения читабельности и удобства сопровождения. Я думаю, что это все еще замечательно, поскольку каждый из них требует от меня думать о предварительных и постусловиях функций, и они действительно помогают обнаруживать ошибки. Тем не менее, я просто хотел представить это, чтобы спросить, есть ли лучшая парадигма для обнаружения логических ошибок в случаях, когда необходимо большое количество проверок.
Комментарий Emacs : Поскольку Emacs является моей предпочтительной средой разработки, я слегка выделяю утверждения assert, которые помогают уменьшить ощущение беспорядка, которое они могут предоставить. Вот что я добавляю в мой файл .emacs:
; gray out the "assert(...)" wrapper
(add-hook 'c-mode-common-hook
(lambda () (font-lock-add-keywords nil
'(("\\<\\(assert\(.*\);\\)" 1 '(:foreground "#444444") t)))))
; gray out the stuff inside parenthesis with a slightly lighter color
(add-hook 'c-mode-common-hook
(lambda () (font-lock-add-keywords nil
'(("\\<assert\\(\(.*\);\\)" 1 '(:foreground "#666666") t)))))
источник
Ответы:
Я видел сотни ошибок, которые были бы устранены быстрее, если бы кто-то написал больше утверждений, и ни одной ошибки, которая была бы устранена быстрее, написав меньше .
Возможно, проблема может быть в читабельности - хотя, по моему опыту, люди, которые пишут хорошие утверждения, также пишут читаемый код. И меня никогда не беспокоит, что начало функции начинается с блока утверждений, чтобы убедиться, что аргументы не являются мусором - просто поставьте пустую строку после нее.
Также, по моему опыту, ремонтопригодность всегда улучшается с помощью утверждений, так же как и с помощью модульных тестов. Утверждения обеспечивают проверку работоспособности того, что код используется так, как предполагалось.
источник
Ну, конечно. [Представьте здесь отвратительный пример.] Однако, применяя руководящие принципы, подробно изложенные ниже, у вас не должно быть проблем с продвижением этого предела на практике. Я тоже большой поклонник утверждений и использую их в соответствии с этими принципами. Большая часть этого совета не является особенной для утверждений, а применяется только к общепринятой хорошей инженерной практике.
Помните время выполнения и двоичные накладные расходы
Утверждения - это здорово, но если они делают вашу программу неприемлемо медленной, это будет либо очень раздражать, либо вы отключите их рано или поздно.
Мне нравится оценивать стоимость утверждения относительно стоимости функции, в которой оно содержится. Рассмотрим следующие два примера.
Сама функция является операцией O (1), но утверждения учитывают O ( n) накладные расходы ). Я не думаю, что вы хотели бы, чтобы такие проверки были активными, за исключением особых случаев.
Вот еще одна функция с похожими утверждениями.
Сама функция является O ( n операцией ), поэтому гораздо больно добавлять дополнительные издержки O ( n ) для утверждения. Замедление функции с помощью небольшого (в данном случае, вероятно, менее 3) постоянного фактора - это то, что мы обычно можем себе позволить в отладочной сборке, но, возможно, не в сборке выпуска.
Теперь рассмотрим этот пример.
В то время как многим людям, вероятно, будет гораздо удобнее это утверждение O (1), чем двум O ( n) утверждениям ) в предыдущем примере, на мой взгляд, они морально эквивалентны. Каждый добавляет накладные расходы на порядок сложности самой функции.
Наконец, существуют «действительно дешевые» утверждения, в которых преобладает сложность функции, в которой они содержатся.
Здесь у нас есть два O (1) утверждения в O ( n ) -функции. Вероятно, не будет проблемой удерживать эти накладные расходы даже в релизных сборках.
Имейте в виду, однако, что асимптотические сложности не всегда дают адекватную оценку, потому что на практике мы всегда имеем дело с размерами входных данных, ограниченными некоторыми конечными постоянными и постоянными факторами, скрытыми «Большой O », которые вполне могут быть незначительными.
Итак, теперь мы определили разные сценарии, что мы можем с ними сделать? (Возможно, слишком) простой подход состоит в том, чтобы следовать правилу, например «Не используйте утверждения, которые доминируют в функции, в которой они содержатся». Хотя это может работать для некоторых проектов, другим может потребоваться более дифференцированный подход. Это можно сделать, используя разные макросы утверждений для разных случаев.
Теперь вы можете использовать три макроса
MY_ASSERT_LOW
,MY_ASSERT_MEDIUM
иMY_ASSERT_HIGH
вместо стандартногоassert
макроса «один размер подходит всем» для утверждений, в которых доминируют, не доминируют и не доминируют и не доминируют над сложностью их содержащей функции соответственно. Когда вы создаете программное обеспечение, вы можете предварительно определить символ препроцессора,MY_ASSERT_COST_LIMIT
чтобы выбрать, какие утверждения должны делать его в исполняемом файле. КонстантыMY_ASSERT_COST_NONE
иMY_ASSERT_COST_ALL
не соответствуют никаким макросам утверждений и предназначены для использования в качестве значений дляMY_ASSERT_COST_LIMIT
того, чтобы выключить или включить все утверждения соответственно.Здесь мы полагаем, что хороший компилятор не будет генерировать код для
и преобразовать
в
что я считаю безопасным предположением в настоящее время.
Если вы собираетесь настроить вышеприведенный код, рассмотрите аннотации, специфичные для компилятора, например
__attribute__ ((cold))
onmy::assertion_failed
или__builtin_expect(…, false)
on,!(CONDITION)
чтобы уменьшить накладные расходы на передаваемые утверждения. В сборках релизов вы также можете рассмотреть замену вызова функции наmy::assertion_failed
чем-то вроде__builtin_trap
уменьшения следа при неудобстве потери диагностического сообщения.Эти виды оптимизации действительно актуальны только в чрезвычайно дешевых утверждениях (например, при сравнении двух целых чисел, которые уже приведены в качестве аргументов) в функции, которая сама по себе очень компактна, не принимая во внимание дополнительный размер двоичного файла, накопленный путем включения всех строк сообщения.
Сравните, как этот код
компилируется в следующую сборку
пока следующий код
дает эту сборку
с которым я чувствую себя намного комфортнее. (Примеры были протестированы с помощью GCC 5.3.0 с помощью
-std=c++14
,-O3
и-march=native
флаги на 4.3.3-2-ARCH x86_64 GNU / Linux. Не показано в приведенных выше фрагментах являются заявленияtest::positive_difference_1st
иtest::positive_difference_2nd
который я добавил__attribute__ ((hot))
к.my::assertion_failed
Был объявлен с__attribute__ ((cold))
.)Утвердить предпосылки в функции, которая зависит от них
Предположим, у вас есть следующая функция с указанным контрактом.
Вместо того чтобы писать
на каждом сайте вызова, поместите эту логику один раз в определение
count_letters
и называть это без лишних слов.
Это имеет следующие преимущества.
assert
операторов в вашем коде.Очевидным недостатком является то, что вы не получите исходное местоположение сайта вызова в диагностическом сообщении. Я считаю, что это незначительная проблема. Хороший отладчик должен иметь возможность удобно отслеживать происхождение нарушения договора.
То же самое относится и к «специальным» функциям, таким как перегруженные операторы. Когда я пишу итераторы, я обычно - если природа итератора это позволяет - даю им функцию-член
это позволяет спросить, безопасно ли разыменовывать итератор. (Конечно, на практике это почти всегда возможно только гарантии , что она не будет в безопасности разыменования итератора. Но я верю , что вы все еще можете поймать много ошибок с такой функцией.) Вместо того , захламление все мой код который использует итератор с
assert(iter.good())
утверждениями, я бы предпочел поставить одинassert(this->good())
в качестве первой строкиoperator*
в реализации итератора.Если вы используете стандартную библиотеку, вместо того чтобы вручную указывать ее предварительные условия в исходном коде, включите их проверки в отладочных сборках. Они могут выполнять даже более сложные проверки, например, проверять, существует ли еще контейнер, на который ссылается итератор. (См. Документацию для libstdc ++ и libc ++ (работа в процессе) для получения дополнительной информации.)
Фактор общих условий
Предположим, вы пишете пакет линейной алгебры. Многие функции будут иметь сложные предварительные условия, и их нарушение часто приводит к неправильным результатам, которые не сразу распознаются как таковые. Было бы очень хорошо, если бы эти функции утверждали свои предварительные условия. Если вы определите группу предикатов, которые сообщают вам определенные свойства о структуре, эти утверждения становятся намного более читабельными.
Это также даст более полезные сообщения об ошибках.
помогает намного больше, чем, скажем,
где вы сначала должны пойти посмотреть исходный код в контексте, чтобы выяснить, что на самом деле было проверено.
Если у тебя есть
class
с нетривиальными инвариантами, то, вероятно, будет хорошей идеей время от времени утверждать их, когда вы перепутались с внутренним состоянием и хотите убедиться, что вы возвращаете объект в действительное состояние по возвращении.Для этой цели я нашел полезным определить функцию-
private
член, которую я обычно вызываюclass_invaraiants_hold_
. Предположим, что вы повторно внедряетеstd::vector
(поскольку мы все знаем, что это недостаточно хорошо), возможно, у него есть такая функция.Обратите внимание на несколько вещей по этому поводу.
const
иnoexcept
в соответствии с руководящими принципами утверждает, что утверждения не должны иметь побочных эффектов. Если это имеет смысл, также объявите этоconstexpr
.assert(this->class_invariants_hold_())
. Таким образом, если утверждения компилируются, мы можем быть уверены, что не возникнет никаких накладных расходов во время выполнения.if
операторов с раннимreturn
s, а не с большим выражением. Это позволяет легко пройтись по функции в отладчике и выяснить, какая часть инварианта была нарушена при срабатывании утверждения.Не утверждай глупостей
Некоторые вещи просто не имеют смысла утверждать.
Эти утверждения не делают код даже чуть-чуть более читабельным или легче рассуждать. Каждый программист C ++ должен быть достаточно уверенным в том, как
std::vector
работает, чтобы быть уверенным, что приведенный выше код верен, просто взглянув на него. Я не говорю, что вы никогда не должны утверждать о размере контейнера. Если вы добавили или удалили элементы, используя какой-то нетривиальный поток управления, такое утверждение может быть полезным. Но если он просто повторяет то, что было написано в коде без утверждений чуть выше, никакого выигрыша не будет.Также не утверждайте, что библиотечные функции работают правильно.
Если вы так мало доверяете библиотеке, лучше подумайте об использовании другой библиотеки.
С другой стороны, если документация библиотеки не ясна на 100%, и вы обретаете уверенность в ее контрактах, читая исходный код, имеет смысл утверждать об этом «предполагаемом контракте». Если он будет сломан в будущей версии библиотеки, вы заметите это быстро.
Это лучше, чем следующее решение, которое не скажет вам, были ли ваши предположения правильными.
Не злоупотребляйте утверждениями для реализации логики программы
Утверждения следует использовать только для выявления ошибок , которые заслуживают немедленного уничтожения вашего приложения. Их не следует использовать для проверки каких-либо других условий, даже если соответствующая реакция на это условие также будет прекращена немедленно.
Поэтому напишите это ...
…вместо этого.
Также никогда не используйте утверждения для проверки ненадежного ввода или проверки того, что вы
std::malloc
не сделали . Даже если вы знаете, что никогда не отключите утверждения, даже в сборках релиза, утверждение сообщает читателю, что оно проверяет что-то, что всегда верно, учитывая, что программа не содержит ошибок и не имеет видимых побочных эффектов. Если это не тот тип сообщения, которое вы хотите передать, используйте альтернативный механизм обработки ошибок, например , исключение. Если вам удобно иметь макропакет для проверок без утверждений, продолжайте писать. Только не называйте это «утверждать», «предполагать», «требовать», «обеспечивать» или что-то в этом роде. Его внутренняя логика может быть такой же, как для , за исключением того, что она, конечно, никогда не компилируется.return
nullptr
throw
assert
Больше информации
Я считаю, что выступление Джона Лакоса « Защитное программирование сделано правильно» , данное на CppCon'14 ( 1- я часть , 2- я часть ), очень поучительно. Он берет идею настроить, какие утверждения включены и как реагировать на неудачные исключения даже дальше, чем я в этом ответе.
источник
Assertions are great, but ... you will turn them off sooner or later.
- Надеюсь, раньше, как прежде, чем код корабли. Вещи, которые должны заставить программу умереть в процессе производства, должны быть частью «реального» кода, а не утверждениями.Я обнаружил, что со временем я пишу меньше утверждений, потому что многие из них означают «работает ли компилятор» и «работает ли библиотека». Когда вы начнете думать о том, что именно вы тестируете, я подозреваю, что вы напишите меньше утверждений.
Например, метод, который (скажем) добавляет что-то в коллекцию, не должен утверждать, что коллекция существует - это обычно либо предварительное условие класса, которому принадлежит сообщение, либо это фатальная ошибка, которая должна вернуть его пользователю , Так что проверьте это один раз, очень рано, а затем примите это.
Утверждения для меня являются инструментом отладки, и я обычно буду использовать их двумя способами: обнаружение ошибки на моем столе (и они не проверяются. Ну, возможно, один из ключевых); и нахождение ошибки на столе клиента (и они действительно проверены). Оба раза я использую утверждения главным образом для генерации трассировки стека после форсирования исключения как можно раньше. Имейте в виду, что утверждения, использованные таким способом, могут легко привести к возникновению ошибок, поскольку ошибка может никогда не возникнуть в отладочной сборке, для которой включены утверждения.
источник
Слишком мало утверждений: удачи в изменении кода, пронизанного скрытыми предположениями.
Слишком много утверждений: может привести к проблемам с читабельностью и, возможно, к запаху кода. Правильно ли разработан класс, функция, API, если в утверждениях утверждений содержится так много предположений?
Могут также быть утверждения, которые на самом деле ничего не проверяют или не проверяют такие вещи, как настройки компилятора в каждой функции: /
Стремитесь к сладкому пятну, но не меньше (как уже сказал кто-то, «больше» утверждений менее вредно, чем слишком мало, или бог нам поможет - нет).
источник
Было бы здорово, если бы вы могли написать функцию Assert, которая брала бы только ссылку на логический метод CONST, таким образом вы уверены, что ваши утверждения не имеют побочных эффектов, гарантируя, что логический метод const используется для тестирования assert
это немного вытянет из читабельности, особенно потому, что я не думаю, что вы не можете аннотировать лямбду (в c ++ 0x), чтобы быть const для какого-то класса, то есть вы не можете использовать лямбда для этого
излишне, если вы спросите меня, но если бы я начал видеть определенный уровень загрязнения из-за утверждений, я бы остерегался двух вещей:
источник
Я написал на C # гораздо больше, чем на C ++, но эти два языка не так уж далеки друг от друга. В .Net я использую Asserts для условий, которые не должны возникать, но я также часто бросаю исключения, когда нет возможности продолжить. Отладчик VS2010 показывает мне много полезной информации об исключении, независимо от того, насколько оптимизирована сборка Release. Также неплохо добавить модульные тесты, если можете. Иногда логирование также полезно иметь в качестве средства отладки.
Так может ли быть слишком много утверждений? Да. Выбор между Прервать / Пропустить / Продолжить 15 раз за одну минуту раздражает. Исключение выдается только один раз. Трудно количественно определить точку, в которой слишком много утверждений, но если ваши утверждения выполняют роль утверждений, исключений, модульных тестов и регистрации, то что-то не так.
Я бы зарезервировал утверждения для сценариев, которых не должно быть. Первоначально вы можете переусердствовать, потому что утверждения быстрее пишутся, но позже переформулируйте код - превратите некоторые из них в исключения, некоторые в тесты и т. Д. Если у вас достаточно дисциплины для очистки каждого комментария TODO, оставьте оставьте комментарий рядом с каждым, который вы планируете переделать, и НЕ ЗАБУДЬТЕ обратиться к TODO позже.
источник
Я хочу работать с тобой! Кто-то, кто пишет много,
asserts
фантастический. Я не знаю, есть ли такая вещь, как "слишком много". Гораздо более распространенными для меня являются люди, которые пишут слишком мало и в конечном итоге сталкиваются со случайной смертельной проблемой UB, которая проявляется только в полнолуние, которое можно было бы легко воспроизвести многократно простымassert
.Сообщение об ошибке
Единственное, о чем я могу подумать, это вставить информацию об ошибках в,
assert
если вы еще этого не делаете, вот так:Таким образом, вы, возможно, больше не будете чувствовать, что у вас их слишком много, если вы еще этого не делали, поскольку теперь вы заставляете свои утверждения играть более важную роль в документировании предположений и предварительных условий.
Побочные эффекты
Конечно, на
assert
самом деле можно неправильно использовать и вводить ошибки, например, так:... если
foo()
вызывает побочные эффекты, так что вы должны быть очень осторожны с этим, но я уверен, что вы уже как тот, кто утверждает очень либерально ("опытный утверждающий"). Надеемся, что ваша процедура тестирования также хороша, как и ваше пристальное внимание к предположениям.Скорость отладки
Хотя скорость отладки, как правило, должна быть в нижней части нашего списка приоритетов, однажды я все-таки утвердил так много в кодовой базе, прежде чем закончился запуск отладочной сборки через отладчик. 100 раз медленнее, чем выпуск.
Это было прежде всего потому, что у меня были такие функции:
... где каждый вызов
operator[]
будет делать утверждение проверки границ. Я закончил тем, что заменил некоторые из этих критичных к производительности небезопасными эквивалентами, которые не утверждают просто радикальное ускорение сборки отладки с минимальными затратами только для безопасности на уровне детализации реализации, и только потому, что начался скачок скорости. очень заметно снизить производительность (выигрыш от ускорения отладки перевешивает стоимость потери нескольких утверждений, но только для таких функций, как эта функция кросс-продукта, которая использовалась в самых критических, измеренных путях, а неoperator[]
в целом).Принцип единой ответственности
Хотя я не думаю, что вы действительно можете ошибиться с большим количеством утверждений (по крайней мере, это намного, гораздо лучше ошибиться на стороне слишком многих, чем слишком мало), сами утверждения могут быть не проблемой, но могут указывать на них.
Например, если у вас есть 5 утверждений для одного вызова функции, это может быть слишком много. Его интерфейс может иметь слишком много предварительных условий и входных параметров, например, я считаю, что это не связано только с темой того, что представляет собой здоровое количество утверждений (на которые я обычно отвечаю «чем больше, тем лучше!»), Но это может быть возможный красный флаг (или очень возможно нет).
источник
Разумно добавлять проверки в ваш код. Для простого подтверждения (встроенного в компилятор C и C ++) мой шаблон использования таков, что неудачное утверждение означает, что в коде есть ошибка, которую необходимо исправить. Я интерпретирую это немного щедро; если я ожидаю , веб - запрос , чтобы вернуть статус 200 и утвердить для него без обработки других случаев , то неудачная утверждение действительно действительно показывает ошибку в моем коде, так утверждают оправдано.
Поэтому, когда люди говорят, что утверждение, проверяющее только то, что делает код, является излишним, это не совсем правильно. Это утверждение проверяет, что, по их мнению, делает код, и весь смысл утверждения заключается в проверке правильности предположения об отсутствии ошибок в коде. И этот документ также может служить документацией. Если я предполагаю, что после выполнения цикла i == n и это не на 100% очевидно из кода, тогда "assert (i == n)" будет полезным.
Лучше иметь больше, чем просто «утверждать» в своем репертуаре, чтобы справиться с различными ситуациями. Например, ситуация, когда я проверяю, что что-то не происходит, что указывает на ошибку, но все еще продолжаю работать вокруг этого условия. (Например, если я использую некоторые кэш , то я мог бы проверить на наличие ошибок, и если ошибка случается неожиданно это может быть безопасным , чтобы исправить ошибку, бросая кэш прочь. Я хочу , чтобы то , что это почти утверждают, что говорит мне во время разработки и все еще позволяет мне продолжить.
Другой пример - ситуация, когда я не ожидаю, что что-то произойдет, у меня есть общий обходной путь, но если это произойдет, я хочу узнать об этом и изучить его. Опять-таки, что-то вроде утверждения, о котором я должен сказать во время разработки. Но не совсем утверждать.
Слишком много утверждений: если утверждение завершает работу вашей программы, когда оно находится в руках пользователя, то у вас не должно быть никаких подтверждений, вызывающих сбой из-за ложных отрицаний.
источник
Это зависит. Если требования к коду четко задокументированы, то утверждение всегда должно соответствовать требованиям. В этом случае это хорошая вещь. Однако, если нет требований или плохо написанных требований, новым программистам будет сложно редактировать код без необходимости каждый раз обращаться к юнит-тесту, чтобы выяснить, каковы требования.
источник