Чи можна написати занадто багато тверджень?


33

Я великий шанувальник написання assertчеків у коді C ++ як способу зафіксувати випадки під час розробки, які неможливо статися, але трапляються через помилки логіки в моїй програмі. Це взагалі хороша практика.

Однак я помітив, що деякі функції, які я записую (які входять до складного класу), мають 5+ тверджень, що вважає, що це потенційно може бути поганою практикою програмування з точки зору читабельності та ремонтопридатності. Я думаю, що це все-таки чудово, тому що кожен вимагає від мене думати про до- і пост-умови функцій, і вони справді допомагають ловити помилки. Однак я просто хотів викласти це там, щоб запитати, чи є кращі парадигми для лога логічних помилок у випадках, коли необхідна велика кількість перевірок.

Коментар Emacs : Оскільки Emacs - це мій IDE, який я вибираю, у мене це злегка затьмарило твердження твердження, що допомагає зменшити відчуття безладу, яке вони можуть надати. Ось що я додаю у свій файл .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 ). Мабуть, це не буде проблемою, щоб утримати це накладні витрати навіть у версії версій.

Але майте на увазі, що асимптотичні складності не завжди дають адекватну оцінку, оскільки на практиці ми завжди маємо справу з розмірами вхідних даних, обмеженими деякими кінцевими постійними та постійними факторами, прихованими “Big- 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))стосуються компілятора, як увімкнено my::assertion_failedабо __builtin_expect(…, false)увімкнено, !(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чи не returnвін nullptr. Навіть якщо ви знаєте, що ви ніколи не відключати твердження, навіть у версії версій, твердження повідомляє читачеві, що він перевіряє те, що завжди відповідає дійсності, враховуючи, що програма без помилок і не має видимих ​​побічних ефектів. Якщо це не той тип повідомлення, яке ви хочете спілкувати, використовуйте альтернативний механізм обробки помилок, наприклад, throwвиняток. Якщо вам зручно мати макро-обгортку для перевірок, що не підтверджуються, заздалегідь напишіть її. Просто не називайте це "стверджувати", "припускати", "вимагати", "забезпечувати" або щось подібне. Його внутрішня логіка може бути такою ж, як і для assert, за винятком того, що вона ніколи не складається, звичайно.

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

Я знайшов розмова Джон LAKOS ' Оборонна Програмування Done Right , враховуючи при CppCon'14 ( 1 - й частини , 2 - й частини ) дуже повчальний. Він бере ідею налаштувати, які твердження ввімкнено, і як реагувати на невдалі винятки навіть далі, ніж я робив у цій відповіді.


4
Assertions are great, but ... you will turn them off sooner or later.- Сподіваюсь, швидше, як перед кодом. Речі, які повинні змусити програму вмирати у виробництві, повинні бути частиною "реального" коду, а не твердженнями.
Blrfl

4

Я вважаю, що з часом я пишу все менше тверджень, оскільки багато з них становлять "компілятор працює" і "працює бібліотека". Як тільки ви почнете думати про те, що саме ви тестуєте, я підозрюю, що ви напишете менше тверджень.

Наприклад, метод, який (скажімо, додає щось до колекції, не повинен стверджувати, що колекція існує - це, як правило, або необхідна умова класу, яким належить повідомлення, або це фатальна помилка, яка повинна повернути його користувачеві . Тому перевіряйте це один раз, дуже рано, тоді припускайте.

Твердження для мене - це інструмент налагодження, і я, як правило, використовую їх двома способами: знайти помилку в моєму столі (і вони не перевіряються. Ну, можливо, один ключ); і виявлення помилки на столі замовника (і вони перевіряються). Обидва рази я використовую твердження здебільшого для створення сліду стека після примусового винятку якомога раніше. Будьте в курсі, що твердження, використані таким чином, можуть легко призвести до heisenbugs - помилка може ніколи не виникати в налагодженні, в якій увімкнено твердження.


4
Я не розумію, що ти кажеш, що "це, як правило, або є умовою класу, якому належить повідомлення, або це фатальна помилка, яка повинна повернути його користувачеві. Тому перевіряйте це один раз, дуже рано, тоді припускайте це. "Для чого ви використовуєте твердження, якщо не для перевірки своїх припущень?
5gon12eder

4

Занадто мало тверджень: удача змінивши цей код, пронизаний прихованими припущеннями.

Занадто багато тверджень: це може призвести до проблем з читабельністю та потенційно запаху коду - чи клас, функція та API розроблені правильно, коли в ньому є так багато припущень, що містяться у твердженнях про затвердження?

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

Намагайтеся на солодке місце, але не менше (як хтось уже сказав, "більше" тверджень менш шкідливо, ніж занадто мало або бог нам допомагає - жодне).


3

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

це може трохи потягнути за читабельністю, тим більше, що я не думаю, що ви не можете коментувати лямбда (в c ++ 0x) як const для якогось класу, тобто ви не можете використовувати лямбда для цього

overkill, якщо ви запитаєте мене, але якщо я почав би бачити певний рівень полюції через твердження, я б насторожено ставився до двох речей:

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

2
Святе лайно вам подобається слово "певний" та його похідні. Я рахую 8 вживань.
Кейсі Паттон

так, вибачте, що я схильний клацати на словах занадто багато - фіксовано, дякую
lurscher

2

Я писав на C # набагато більше, ніж у C ++, але дві мови не дуже далеко одна від одної. У .Net я використовую Asserts для умов, яких не повинно відбуватися, але я також часто кидаю винятки, коли немає можливості продовжувати. Налагоджувач VS2010 показує мені багато корисної інформації про виняток, незалежно від того, наскільки оптимізована збірка випусків. Це також хороша ідея, якщо можна, додати одиничні тести. Іноді ведення журналів також є хорошою справою як налагоджувальна програма.

Отже, чи може бути занадто багато тверджень? Так. Вибір між абортом / ігноруванням / продовженням 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 тверджень для одного виклику функції, це може зробити занадто багато. У його інтерфейсі може бути занадто багато передумов і вхідних параметрів, наприклад, я вважаю, що це не пов'язане лише з темою того, що є здоровим числом тверджень (на які я б, як правило, відповідав, "тим більше, ніж Merrier!"), Але це може бути можливий червоний прапор (або, можливо, ні).


1
Що ж, теоретично може бути «занадто багато», хоча ця проблема стає очевидною дуже швидко: якщо затвердження займає значно більше часу, ніж м'ясо функції. Правда, я не можу пригадати, щоб виявити, що в дикій природі все-таки є протилежна проблема.
Дедуплікатор

@ Так, я зіткнувся з цим випадком у цих критичних векторних математичних процедурах. Хоча це, безумовно, здається набагато краще помилитися на стороні занадто багатьох, ніж занадто мало!

-1

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

Тож коли люди кажуть, що зайве лише те, що перевіряє те, що робить код, зайве, це не зовсім правильно. Цей assrt перевіряє те, що, на їх думку, робить код, і вся суть утвердження полягає в тому, щоб перевірити, що припущення про помилку в коді не є правильним. І аргумент також може послужити документацією. Якщо я припускаю, що після виконання циклу i == n і це не на 100% очевидно з коду, то "assert (i == n)" буде корисним.

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

Інший приклад - ситуація, коли я не очікую, що щось трапиться, я маю загальне вирішення, але якщо ця річ станеться, я хочу знати про це і вивчити це. Знову щось майже схоже на твердження, що повинно сказати мені під час розвитку. Але не зовсім твердження.

Занадто багато тверджень: Якщо ствердження розбиває вашу програму, коли вона знаходиться в руках користувача, тоді у вас не повинно бути жодних тверджень, які виходять з ладу через помилкові негативи.


-3

Це залежить. Якщо вимоги до коду чітко задокументовані, то твердження завжди повинно відповідати вимогам. У цьому випадку це гарна річ. Однак якщо немає жодних вимог або погано написаних вимог, то новим програмістам буде важко редагувати код, не потребуючи кожного разу звертатися до одиничного тесту, щоб з'ясувати, що це за вимоги.


3
це, здається, не пропонує нічого суттєвого щодо питань, викладених та пояснених у попередніх 8 відповідях
gnat
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.