Можно ли написать слишком много утверждений?

33

Я большой поклонник написания 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)))))
Алан Тьюринг
источник
3
Я должен признать, что это вопрос, который приходил мне в голову время от времени. Интересно услышать мнение других по этому поводу.
Капитан Разумный

Ответы:

45

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

[C] мог бы [слишком много утверждений] потенциально быть плохой практикой программирования, с точки зрения читабельности и ремонтопригодности [?]

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

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

Боб Мерфи
источник
1
Хороший ответ. Я также добавил описание к вопросу о том, как улучшить читаемость в Emacs.
Алан Тьюринг
2
«По моему опыту, люди, которые пишут хорошие утверждения, также пишут читаемый код» << превосходное замечание. Делать код читаемым - это зависит от конкретного программиста, так же как и от тех методов, которые он или она используют, и которые ему запрещено использовать. Я видел, как хорошие методы становятся нечитаемыми в чужих руках, и даже то, что большинство считает плохими, становится совершенно ясным, даже элегантным, при правильном использовании абстракции и комментирования.
Грег Джексон,
У меня было несколько сбоев приложений, которые были вызваны ошибочными утверждениями. Так что я видел ошибки , которые не были бы существовали , если кто - то (я) написал меньше утверждает.
CodesInChaos
@CodesInChaos Возможно, кроме опечаток, это указывает на ошибку в формулировке проблемы - то есть, ошибка была в дизайне, следовательно, несоответствие между утверждениями и (другим) кодом.
Лоуренс
12

Можно ли написать слишком много утверждений?

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

Помните время выполнения и двоичные накладные расходы

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

Мне нравится оценивать стоимость утверждения относительно стоимости функции, в которой оно содержится. Рассмотрим следующие два примера.

// Precondition:  queue is not empty
// Invariant:     queue is sorted
template <typename T>
const T&
sorted_queue<T>::max() const noexcept
{
  assert(!this->data_.empty());
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
  return this->data_.back();
}

Сама функция является операцией O (1), но утверждения учитывают O ( n) накладные расходы ). Я не думаю, что вы хотели бы, чтобы такие проверки были активными, за исключением особых случаев.

Вот еще одна функция с похожими утверждениями.

// Requirement:   op : T -> T is monotonic [ie x <= y implies op(x) <= op(y)]
// Invariant:     queue is sorted
// Postcondition: each item x in the queue is replaced by op(x)
template <typename T>
template <typename FuncT>
void
sorted_queue<T>::apply_monotonic_function(FuncT&& op)
{
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
  std::transform(std::cbegin(this->data_), std::cend(this->data_),
                 std::begin(this->data_), std::forward<FuncT>(op));
  assert(std::is_sorted(std::cbegin(this->data_), std::cend(this->data_)));
}

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

Теперь рассмотрим этот пример.

// Precondition:  queue is not empty
// Invariant:     queue is sorted
// Postcondition: last element is removed from queue
template <typename T>
void
sorted_queue<T>::pop_back() noexcept
{
  assert(!this->data_.empty());
  return this->data_.pop_back();
}

В то время как многим людям, вероятно, будет гораздо удобнее это утверждение O (1), чем двум O ( n) утверждениям ) в предыдущем примере, на мой взгляд, они морально эквивалентны. Каждый добавляет накладные расходы на порядок сложности самой функции.

Наконец, существуют «действительно дешевые» утверждения, в которых преобладает сложность функции, в которой они содержатся.

// Requirement:   cmp : T x T -> bool is a strict weak ordering
// Precondition:  queue is not empty
// Postcondition: if x is returned, then there is no y in the queue
//                such that cmp(x, y)
template <typename T>
template <typename CmpT>
const T&
sorted_queue<T>::max(CmpT&& cmp) const
{
  assert(!this->data_.empty());
  const auto pos = std::max_element(std::cbegin(this->data_),
                                    std::cend(this->data_),
                                    std::forward<CmpT>(cmp));
  assert(pos != std::cend(this->data_));
  return *pos;
}

Здесь у нас есть два O (1) утверждения в O ( n ) -функции. Вероятно, не будет проблемой удерживать эти накладные расходы даже в релизных сборках.

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

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

#define MY_ASSERT_IMPL(COST, CONDITION)                                       \
  (                                                                           \
    ( ((COST) <= (MY_ASSERT_COST_LIMIT)) && !(CONDITION) )                    \
      ? ::my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, # CONDITION) \
      : (void) 0                                                              \
  )

#define MY_ASSERT_LOW(CONDITION)                                              \
  MY_ASSERT_IMPL(MY_ASSERT_COST_LOW, CONDITION)

#define MY_ASSERT_MEDIUM(CONDITION)                                           \
  MY_ASSERT_IMPL(MY_ASSERT_COST_MEDIUM, CONDITION)

#define MY_ASSERT_HIGH(CONDITION)                                             \
  MY_ASSERT_IMPL(MY_ASSERT_COST_HIGH, CONDITION)

#define MY_ASSERT_COST_NONE    0
#define MY_ASSERT_COST_LOW     1
#define MY_ASSERT_COST_MEDIUM  2
#define MY_ASSERT_COST_HIGH    3
#define MY_ASSERT_COST_ALL    10

#ifndef MY_ASSERT_COST_LIMIT
#  define MY_ASSERT_COST_LIMIT MY_ASSERT_COST_MEDIUM
#endif

namespace my
{

  [[noreturn]] extern void
  assertion_failed(const char * filename, int line, const char * function,
                   const char * message) noexcept;

}

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

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

if (false_constant_expression && run_time_expression) { /* ... */ }

и преобразовать

if (true_constant_expression && run_time_expression) { /* ... */ }

в

if (run_time_expression) { /* ... */ }

что я считаю безопасным предположением в настоящее время.

Если вы собираетесь настроить вышеприведенный код, рассмотрите аннотации, специфичные для компилятора, например __attribute__ ((cold))on my::assertion_failedили __builtin_expect(…, false)on, !(CONDITION)чтобы уменьшить накладные расходы на передаваемые утверждения. В сборках релизов вы также можете рассмотреть замену вызова функции наmy::assertion_failed чем-то вроде __builtin_trapуменьшения следа при неудобстве потери диагностического сообщения.

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

Сравните, как этот код

int
positive_difference_1st(const int a, const int b) noexcept
{
  if (!(a > b))
    my::assertion_failed(__FILE__, __LINE__, __FUNCTION__, "!(a > b)");
  return a - b;
}

компилируется в следующую сборку

_ZN4test23positive_difference_1stEii:
.LFB0:
        .cfi_startproc
        cmpl    %esi, %edi
        jle     .L5
        movl    %edi, %eax
        subl    %esi, %eax
        ret
.L5:
        subq    $8, %rsp
        .cfi_def_cfa_offset 16
        movl    $.LC0, %ecx
        movl    $_ZZN4test23positive_difference_1stEiiE12__FUNCTION__, %edx
        movl    $50, %esi
        movl    $.LC1, %edi
        call    _ZN2my16assertion_failedEPKciS1_S1_
        .cfi_endproc
.LFE0:

пока следующий код

int
positive_difference_2nd(const int a, const int b) noexcept
{
  if (__builtin_expect(!(a > b), false))
    __builtin_trap();
  return a - b;
}

дает эту сборку

_ZN4test23positive_difference_2ndEii:
.LFB1:
        .cfi_startproc
        cmpl    %esi, %edi
        jle     .L8
        movl    %edi, %eax
        subl    %esi, %eax
        ret
        .p2align 4,,7
        .p2align 3
.L8:
        ud2
        .cfi_endproc
.LFE1:

с которым я чувствую себя намного комфортнее. (Примеры были протестированы с помощью 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)).)

Утвердить предпосылки в функции, которая зависит от них

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

/**
 * @brief
 *         Counts the frequency of a letter in a string.
 *
 * The frequency count is case-insensitive.
 *
 * If `text` does not point to a NUL terminated character array or `letter`
 * is not in the character range `[A-Za-z]`, the behavior is undefined.
 *
 * @param text
 *         text to count the letters in
 *
 * @param letter
 *         letter to count
 *
 * @returns
 *         occurences of `letter` in `text`
 *
 */
std::size_t
count_letters(const char * text, int letter) noexcept;

Вместо того чтобы писать

assert(text != nullptr);
assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
const auto frequency = count_letters(text, letter);

на каждом сайте вызова, поместите эту логику один раз в определение count_letters

std::size_t
count_letters(const char *const text, const int letter) noexcept
{
  assert(text != nullptr);
  assert((letter >= 'A' && letter <= 'Z') || (letter >= 'a' && letter <= 'z'));
  auto frequency = std::size_t {};
  // TODO: Figure this out...
  return frequency;
}

и называть это без лишних слов.

const auto frequency = count_letters(text, letter);

Это имеет следующие преимущества.

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

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

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

bool
good() const noexcept;

это позволяет спросить, безопасно ли разыменовывать итератор. (Конечно, на практике это почти всегда возможно только гарантии , что она не будет в безопасности разыменования итератора. Но я верю , что вы все еще можете поймать много ошибок с такой функцией.) Вместо того , захламление все мой код который использует итератор с assert(iter.good())утверждениями, я бы предпочел поставить одинassert(this->good()) в качестве первой строки operator*в реализации итератора.

Если вы используете стандартную библиотеку, вместо того чтобы вручную указывать ее предварительные условия в исходном коде, включите их проверки в отладочных сборках. Они могут выполнять даже более сложные проверки, например, проверять, существует ли еще контейнер, на который ссылается итератор. (См. Документацию для libstdc ++ и libc ++ (работа в процессе) для получения дополнительной информации.)

Фактор общих условий

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

template <typename MatrixT>
auto
cholesky_decompose(MatrixT&& m)
{
  assert(is_square(m) && is_symmetric(m));
  // TODO: Somehow decompose that thing...
}

Это также даст более полезные сообщения об ошибках.

cholesky.hxx:357: cholesky_decompose: assertion failed: is_symmetric(m)

помогает намного больше, чем, скажем,

detail/basic_ops.hxx:1289: fast_compare: assertion failed: m(i, j) == m(j, i)

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

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

Для этой цели я нашел полезным определить функцию- privateчлен, которую я обычно вызываю class_invaraiants_hold_. Предположим, что вы повторно внедряете std::vector(поскольку мы все знаем, что это недостаточно хорошо), возможно, у него есть такая функция.

template <typename T>
bool
vector<T>::class_invariants_hold_() const noexcept
{
  if (this->size_ > this->capacity_)
    return false;
  if ((this->size_ > 0) && (this->data_ == nullptr))
    return false;
  if ((this->capacity_ == 0) != (this->data_ == nullptr))
    return false;
  return true;
}

Обратите внимание на несколько вещей по этому поводу.

  • Функция предиката сама по себе constи noexceptв соответствии с руководящими принципами утверждает, что утверждения не должны иметь побочных эффектов. Если это имеет смысл, также объявите этоconstexpr .
  • Предикат сам по себе ничего не утверждает. Он предназначен для вызова внутри утверждений, таких как assert(this->class_invariants_hold_()). Таким образом, если утверждения компилируются, мы можем быть уверены, что не возникнет никаких накладных расходов во время выполнения.
  • Поток управления внутри функции разбит на несколько ifоператоров с ранним returns, а не с большим выражением. Это позволяет легко пройтись по функции в отладчике и выяснить, какая часть инварианта была нарушена при срабатывании утверждения.

Не утверждай глупостей

Некоторые вещи просто не имеют смысла утверждать.

auto numbers = std::vector<int> {};
numbers.push_back(14);
numbers.push_back(92);
assert(numbers.size() == 2);  // silly
assert(!numbers.empty());     // silly and redundant

Эти утверждения не делают код даже чуть-чуть более читабельным или легче рассуждать. Каждый программист C ++ должен быть достаточно уверенным в том, какstd::vector работает, чтобы быть уверенным, что приведенный выше код верен, просто взглянув на него. Я не говорю, что вы никогда не должны утверждать о размере контейнера. Если вы добавили или удалили элементы, используя какой-то нетривиальный поток управления, такое утверждение может быть полезным. Но если он просто повторяет то, что было написано в коде без утверждений чуть выше, никакого выигрыша не будет.

Также не утверждайте, что библиотечные функции работают правильно.

auto w = widget {};
w.enable_quantum_mode();
assert(w.quantum_mode_enabled());  // probably silly

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

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

auto w = widget {};
// After reading the source code, I have concluded that quantum mode is
// always off by default but this isn't documented anywhere.
assert(!w.quantum_mode_enabled());

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

auto w = widget {};
if (w.quantum_mode_enabled())
  {
    // I don't think that quantum mode is ever enabled by default but
    // I'm not sure.
    w.disable_quantum_mode();
  }

Не злоупотребляйте утверждениями для реализации логики программы

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

Поэтому напишите это ...

if (!server_reachable())
  {
    log_message("server not reachable");
    shutdown();
  }

…вместо этого.

assert(server_reachable());

Также никогда не используйте утверждения для проверки ненадежного ввода или проверки того, что вы std::mallocне сделали . Даже если вы знаете, что никогда не отключите утверждения, даже в сборках релиза, утверждение сообщает читателю, что оно проверяет что-то, что всегда верно, учитывая, что программа не содержит ошибок и не имеет видимых побочных эффектов. Если это не тот тип сообщения, которое вы хотите передать, используйте альтернативный механизм обработки ошибок, например , исключение. Если вам удобно иметь макропакет для проверок без утверждений, продолжайте писать. Только не называйте это «утверждать», «предполагать», «требовать», «обеспечивать» или что-то в этом роде. Его внутренняя логика может быть такой же, как для , за исключением того, что она, конечно, никогда не компилируется.returnnullptrthrowassert

Больше информации

Я считаю, что выступление Джона Лакоса « Защитное программирование сделано правильно» , данное на CppCon'14 ( 1- я часть , 2- я часть ), очень поучительно. Он берет идею настроить, какие утверждения включены и как реагировать на неудачные исключения даже дальше, чем я в этом ответе.

5gon12eder
источник
4
Assertions are great, but ... you will turn them off sooner or later.- Надеюсь, раньше, как прежде, чем код корабли. Вещи, которые должны заставить программу умереть в процессе производства, должны быть частью «реального» кода, а не утверждениями.
Blrfl
4

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

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

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


источник
4
Я не понимаю вашу точку зрения, когда вы говорите: «Обычно это либо предварительное условие класса, которому принадлежит сообщение, либо фатальная ошибка, которая должна вернуть его пользователю. Поэтому проверьте это один раз, очень рано, а затем примите это ». Для чего вы используете утверждения, если не для проверки своих предположений?
5gon12eder
4

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

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

Могут также быть утверждения, которые на самом деле ничего не проверяют или не проверяют такие вещи, как настройки компилятора в каждой функции: /

Стремитесь к сладкому пятну, но не меньше (как уже сказал кто-то, «больше» утверждений менее вредно, чем слишком мало, или бог нам поможет - нет).

MaR
источник
3

Было бы здорово, если бы вы могли написать функцию Assert, которая брала бы только ссылку на логический метод CONST, таким образом вы уверены, что ваши утверждения не имеют побочных эффектов, гарантируя, что логический метод const используется для тестирования assert

это немного вытянет из читабельности, особенно потому, что я не думаю, что вы не можете аннотировать лямбду (в c ++ 0x), чтобы быть const для какого-то класса, то есть вы не можете использовать лямбда для этого

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

  • убедившись, что в assert не происходит побочных эффектов (предоставляемых конструкцией, как описано выше)
  • производительность при тестировании разработки; это можно решить, добавив уровни (например, ведение журнала) к средству assert; так что вы можете отключить некоторые утверждения из сборки разработки для повышения производительности
lurscher
источник
2
Святое дерьмо, тебе нравится слово «определенный» и его производные. Я считаю 8 использует.
Кейси Паттон
да, извините, я слишком много
кликаю
2

Я написал на C # гораздо больше, чем на C ++, но эти два языка не так уж далеки друг от друга. В .Net я использую Asserts для условий, которые не должны возникать, но я также часто бросаю исключения, когда нет возможности продолжить. Отладчик VS2010 показывает мне много полезной информации об исключении, независимо от того, насколько оптимизирована сборка Release. Также неплохо добавить модульные тесты, если можете. Иногда логирование также полезно иметь в качестве средства отладки.

Так может ли быть слишком много утверждений? Да. Выбор между Прервать / Пропустить / Продолжить 15 раз за одну минуту раздражает. Исключение выдается только один раз. Трудно количественно определить точку, в которой слишком много утверждений, но если ваши утверждения выполняют роль утверждений, исключений, модульных тестов и регистрации, то что-то не так.

Я бы зарезервировал утверждения для сценариев, которых не должно быть. Первоначально вы можете переусердствовать, потому что утверждения быстрее пишутся, но позже переформулируйте код - превратите некоторые из них в исключения, некоторые в тесты и т. Д. Если у вас достаточно дисциплины для очистки каждого комментария TODO, оставьте оставьте комментарий рядом с каждым, который вы планируете переделать, и НЕ ЗАБУДЬТЕ обратиться к TODO позже.

работа
источник
Если ваш код не выполняет 15 утверждений в минуту, я думаю, что это более серьезная проблема. Утверждения никогда не должны запускаться в безошибочном коде, а если они это делают, они должны убивать приложение, чтобы предотвратить дальнейшее повреждение, или бросить вас в отладчик, чтобы посмотреть, что происходит.
5gon12eder
2

Я хочу работать с тобой! Кто-то, кто пишет много, assertsфантастический. Я не знаю, есть ли такая вещь, как "слишком много". Гораздо более распространенными для меня являются люди, которые пишут слишком мало и в конечном итоге сталкиваются со случайной смертельной проблемой UB, которая проявляется только в полнолуние, которое можно было бы легко воспроизвести многократно простым assert.

Сообщение об ошибке

Единственное, о чем я могу подумать, это вставить информацию об ошибках в, assertесли вы еще этого не делаете, вот так:

assert(n >= 0 && n < num && "Index is out of bounds.");

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

Побочные эффекты

Конечно, на assertсамом деле можно неправильно использовать и вводить ошибки, например, так:

assert(foo() && "Call to foo failed!");

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

Скорость отладки

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

Это было прежде всего потому, что у меня были такие функции:

vec3f cross_product(const vec3f& lhs, const vec3f& rhs)
{
    return vec3f
    (
        lhs[1] * rhs[2] - lhs[2] * rhs[1],
        lhs[2] * rhs[0] - lhs[0] * rhs[2],
        lhs[0] * rhs[1] - lhs[1] * rhs[0]
    );
}

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

Принцип единой ответственности

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

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


источник
1
Что ж, в теории может быть «слишком много» утверждений, хотя эта проблема становится очевидной очень быстро: если утверждение занимает значительно больше времени, чем основная часть функции. Правда, я не могу вспомнить, что обнаружил, что в дикой природе все еще встречается противоположная проблема.
Дедупликатор
@Deduplicator А, да, я сталкивался с этим случаем в этих критических математических программах. Хотя определенно кажется, что лучше ошибаться на стороне слишком многих, чем слишком немногих!
-1

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

Поэтому, когда люди говорят, что утверждение, проверяющее только то, что делает код, является излишним, это не совсем правильно. Это утверждение проверяет, что, по их мнению, делает код, и весь смысл утверждения заключается в проверке правильности предположения об отсутствии ошибок в коде. И этот документ также может служить документацией. Если я предполагаю, что после выполнения цикла i == n и это не на 100% очевидно из кода, тогда "assert (i == n)" будет полезным.

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

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

Слишком много утверждений: если утверждение завершает работу вашей программы, когда оно находится в руках пользователя, то у вас не должно быть никаких подтверждений, вызывающих сбой из-за ложных отрицаний.

gnasher729
источник
-3

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

Cucky Arabi
источник
3
это, кажется, не предлагает ничего существенного по сравнению с замечаниями, сделанными и объясненными в предыдущих 8 ответах
комнат