Kann man zu viele Aussagen schreiben?

33

Ich bin ein großer Fan des Schreibens von assertPrüfungen in C ++ - Code, um Fälle während der Entwicklung abzufangen, die möglicherweise nicht auftreten können, aber aufgrund von Logikfehlern in meinem Programm auftreten. Dies ist im Allgemeinen eine gute Praxis.

Ich habe jedoch festgestellt, dass einige von mir geschriebene Funktionen (die Teil einer komplexen Klasse sind) Asserts über 5 aufweisen, was hinsichtlich der Lesbarkeit und Wartbarkeit möglicherweise eine schlechte Programmierpraxis darstellt. Ich finde es immer noch großartig, da jeder von mir verlangt, über die Vor- und Nachbedingungen von Funktionen nachzudenken, und sie helfen wirklich dabei, Fehler zu finden. Ich wollte dies jedoch nur veröffentlichen, um zu fragen, ob es ein besseres Paradigma gibt, um logische Fehler zu erkennen, wenn eine große Anzahl von Überprüfungen erforderlich ist.

Emacs-Kommentar : Da Emacs meine bevorzugte IDE ist, sind die Assert-Anweisungen etwas grau hinterlegt, wodurch das Gefühl von Unordnung, das sie erzeugen können, verringert wird. Folgendes füge ich meiner .emacs-Datei hinzu:

; 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)))))
Alan Turing
quelle
3
Ich muss zugeben, dass dies eine Frage ist, die mir immer wieder in den Sinn gekommen ist. Interessiert, die Meinungen anderer dazu zu hören.
Captain Sensible

Antworten:

45

Ich habe Hunderte von Fehlern gesehen, die schneller behoben worden wären, wenn jemand mehr Behauptungen geschrieben hätte, und keine einzige, die schneller behoben worden wäre, wenn weniger geschrieben worden wäre .

[C] Könnte [zu viele Behauptungen] möglicherweise eine schlechte Programmierpraxis in Bezug auf Lesbarkeit und Wartbarkeit sein [?]

Vielleicht könnte die Lesbarkeit ein Problem sein - obwohl ich die Erfahrung gemacht habe, dass Leute, die gute Aussagen machen, auch lesbaren Code schreiben. Und es stört mich nie, wenn der Beginn einer Funktion mit einem Block von Zusicherungen beginnt, um sicherzustellen, dass die Argumente kein Müll sind - setzen Sie einfach eine leere Zeile dahinter.

Auch nach meiner Erfahrung wird die Wartbarkeit durch Asserts immer verbessert, genauso wie durch Unit-Tests. Asserts bieten eine Überprüfung, ob der Code so verwendet wird, wie er verwendet werden soll.

Bob Murphy
quelle
1
Gute Antwort. Ich habe auch der Frage, wie ich die Lesbarkeit mit Emacs verbessere, eine Beschreibung hinzugefügt.
Alan Turing
2
"Ich habe die Erfahrung gemacht, dass Leute, die gute Aussagen machen, auch lesbaren Code schreiben" << excellent point. Das Lesen von Code ist für den einzelnen Programmierer ebenso selbstverständlich wie für die Techniken, die er oder sie verwendet und die er nicht verwenden darf. Ich habe gesehen, dass gute Techniken in den falschen Händen unlesbar werden, und selbst was die meisten als schlechte Techniken betrachten, wird durch die richtige Verwendung von Abstraktion und Kommentierung vollkommen klar und sogar elegant.
Greg Jackson
Ich hatte einige Anwendungsabstürze, die durch falsche Behauptungen verursacht wurden. Ich habe also Fehler gesehen, die es nicht gegeben hätte, wenn jemand (ich) weniger Behauptungen geschrieben hätte.
CodesInChaos
@CodesInChaos Abgesehen von Tippfehlern deutet dies vermutlich auf einen Fehler bei der Formulierung des Problems hin - das heißt, der Fehler lag im Design, daher die Nichtübereinstimmung zwischen Behauptungen und (anderem) Code.
Lawrence
12

Kann man zu viele Aussagen schreiben?

Nun, natürlich ist es das. [Stellen Sie sich hier ein abscheuliches Beispiel vor.] Wenn Sie jedoch die im Folgenden beschriebenen Richtlinien anwenden, sollten Sie in der Praxis keine Probleme haben, diese Grenze zu überschreiten. Ich bin auch ein großer Fan von Behauptungen und verwende sie nach diesen Grundsätzen. Ein Großteil dieser Ratschläge bezieht sich nicht auf Behauptungen, sondern nur auf die allgemein anerkannten Regeln der Technik.

Beachten Sie den Aufwand für Laufzeit und Binärdaten

Behauptungen sind großartig, aber wenn sie Ihr Programm inakzeptabel verlangsamen, wird es entweder sehr ärgerlich sein oder Sie werden sie früher oder später deaktivieren.

Ich möchte die Kosten einer Behauptung im Verhältnis zu den Kosten der Funktion messen, in der sie enthalten ist. Betrachten Sie die folgenden zwei Beispiele.

// 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();
}

Die Funktion selbst ist eine O (1) -Operation, aber die Zusicherungen berücksichtigen den O ( n ) -Overhead. Ich glaube nicht, dass Sie möchten, dass solche Überprüfungen nur unter ganz besonderen Umständen durchgeführt werden.

Hier ist eine andere Funktion mit ähnlichen Behauptungen.

// 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_)));
}

Die Funktion selbst ist eine O ( n ) -Operation, daher tut es viel weniger weh, einen zusätzlichen O ( n ) -Overhead für die Zusicherung hinzuzufügen . Eine Funktion um einen kleinen (in diesem Fall wahrscheinlich weniger als 3) konstanten Faktor zu verlangsamen, ist etwas, das wir uns normalerweise in einem Debug-Build leisten können, aber möglicherweise nicht in einem Release-Build.

Betrachten Sie nun dieses Beispiel.

// 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();
}

Während sich viele Menschen mit dieser O (1) -Aussage wahrscheinlich viel wohler fühlen als mit den beiden O ( n ) -Aussagen im vorherigen Beispiel, sind sie meiner Ansicht nach moralisch gleichwertig. Jeder fügt Overhead in der Reihenfolge der Komplexität der Funktion selbst hinzu.

Schließlich gibt es die "wirklich billigen" Behauptungen, die von der Komplexität der Funktion, in der sie enthalten sind, dominiert werden.

// 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;
}

Hier haben wir zwei O (1) -Aussagen in einer O ( n ) -Funktion. Es ist wahrscheinlich kein Problem, diesen Overhead auch in Release-Builds beizubehalten.

Bedenken Sie jedoch, dass asymptotische Komplexitäten nicht immer eine angemessene Schätzung liefern, da es sich in der Praxis immer um Eingabegrößen handelt, die durch einige endliche konstante und konstante Faktoren begrenzt sind, die von „Big- O “ verborgen werden .

Nun haben wir verschiedene Szenarien identifiziert. Was können wir dagegen tun? Ein (wahrscheinlich zu) einfacher Ansatz wäre die Befolgung einer Regel wie „Verwenden Sie keine Aussagen, die die Funktion dominieren, in der sie enthalten sind.“ Während dies für einige Projekte möglicherweise funktioniert, benötigen andere möglicherweise einen differenzierteren Ansatz. Dies könnte durch Verwendung verschiedener Zusicherungsmakros für die verschiedenen Fälle geschehen.

#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;

}

Sie können nun die drei Makros verwendet MY_ASSERT_LOW, MY_ASSERT_MEDIUMund MY_ASSERT_HIGHanstelle der Standard - Bibliothek „one size fits all“ assertMakro für Behauptungen , die von beherrscht werden, weder dominiert noch dominiert und die Komplexität ihrer Funktion enthält , dominiert sind. Beim Erstellen der Software können Sie das Präprozessorsymbol vordefinieren, MY_ASSERT_COST_LIMITum auszuwählen, welche Art von Zusicherungen es in die ausführbare Datei aufnehmen soll. Die Konstanten MY_ASSERT_COST_NONEund MY_ASSERT_COST_ALLentsprechen keinen Assert-Makros und dienen als Werte für MY_ASSERT_COST_LIMIT, um alle Assertions zu aktivieren bzw. zu deaktivieren.

Wir gehen hier von der Annahme aus, dass ein guter Compiler keinen Code für generiert

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

und transformieren

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

in

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

was ich heutzutage für eine sichere Annahme halte.

Wenn Sie den obigen Code optimieren möchten, sollten Sie compilerspezifische Anmerkungen wie " __attribute__ ((cold))on" my::assertion_failedoder " __builtin_expect(…, false)on" berücksichtigen !(CONDITION), um den Aufwand für übergebene Zusicherungen zu verringern. In Release-Builds können Sie auch in Betracht ziehen, den Funktionsaufruf auf my::assertion_faileddurch etwas __builtin_trapzu ersetzen, das den Platzbedarf verringert , wenn Sie eine Diagnosemeldung verlieren.

Diese Art von Optimierungen ist wirklich nur bei extrem billigen Zusicherungen (wie dem Vergleichen von zwei Ganzzahlen, die bereits als Argumente angegeben sind) in einer Funktion relevant, die selbst sehr kompakt ist, ohne die zusätzliche Größe der durch das Einbeziehen aller Nachrichtenzeichenfolgen akkumulierten Binärdatei zu berücksichtigen.

Vergleichen Sie, wie dieser Code

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;
}

wird in die folgende Assembly kompiliert

_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:

während der folgende Code

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

gibt diese Versammlung

_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:

Womit ich mich viel wohler fühle. (Beispiele wurden mit GCC 5.3.0 getestet mit der -std=c++14, -O3und -march=nativeFahnen auf 4.3.3-2-ARCH x86_64 GNU / Linux. Nicht in den oben genannten Schnipsel gezeigt sind die Erklärungen test::positive_difference_1stund test::positive_difference_2nddie ich hinzugefügt die __attribute__ ((hot))zu. my::assertion_failedErklärt wurde , mit __attribute__ ((cold)).)

Legen Sie in der von ihnen abhängigen Funktion die Voraussetzungen fest

Angenommen, Sie haben die folgende Funktion mit dem angegebenen Vertrag.

/**
 * @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;

Anstatt zu schreiben

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

Schreiben Sie diese Logik an jeder Aufrufstelle einmal in die Definition von 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;
}

und nenne es ohne weiteres.

const auto frequency = count_letters(text, letter);

Dies hat folgende Vorteile.

  • Sie müssen den Assertionscode nur einmal schreiben. Da der eigentliche Zweck von Funktionen darin besteht, dass sie - oftmals mehrmals - aufgerufen werden, sollte dies die Gesamtzahl der assertAnweisungen in Ihrem Code verringern .
  • Es hält die Logik, die die Voraussetzungen überprüft, nahe an der Logik, die von ihnen abhängt. Ich denke, das ist der wichtigste Aspekt. Wenn Ihre Clients Ihre Benutzeroberfläche missbrauchen, kann nicht davon ausgegangen werden, dass sie die Behauptungen korrekt anwenden. Daher ist es besser, wenn die Funktion sie darüber informiert.

Der offensichtliche Nachteil ist, dass Sie den Quellort der Anrufstelle nicht in die Diagnosemeldung aufnehmen können. Ich glaube, dass dies ein kleines Problem ist. Ein guter Debugger sollte es Ihnen ermöglichen, die Ursache der Vertragsverletzung auf bequeme Weise nachzuvollziehen.

Dasselbe gilt für „spezielle“ Funktionen wie überladene Operatoren. Wenn ich Iteratoren schreibe, gebe ich ihnen normalerweise - wenn die Art des Iterators dies zulässt - eine Member-Funktion

bool
good() const noexcept;

das erlaubt zu fragen, ob es sicher ist, den Iterator zu dereferenzieren. (Natürlich kann in der Praxis fast immer nur garantiert werden, dass es nicht sicher ist, den Iterator zu dereferenzieren. Aber ich glaube, Sie können mit einer solchen Funktion immer noch viele Fehler finden.) Anstatt meinen gesamten Code zu verunreinigen Wenn der Iterator mit assert(iter.good())Anweisungen verwendet wird, möchte ich lieber eine einzelne assert(this->good())Zeile als erste Zeile operator*in die Implementierung des Iterators einfügen.

Wenn Sie die Standardbibliothek verwenden, müssen Sie deren Prüfungen in Debugbuilds aktivieren, anstatt die Voraussetzungen im Quellcode manuell festzulegen. Sie können noch komplexere Prüfungen durchführen, beispielsweise testen, ob der Container, auf den sich ein Iterator bezieht, noch vorhanden ist. ( Weitere Informationen finden Sie in der Dokumentation zu libstdc ++ und libc ++ (in Arbeit).)

Faktor gemeinsame Bedingungen aus

Angenommen, Sie schreiben ein lineares Algebra-Paket. Viele Funktionen haben komplizierte Voraussetzungen, und wenn sie verletzt werden, entstehen häufig falsche Ergebnisse, die nicht sofort als solche erkennbar sind. Es wäre sehr gut, wenn diese Funktionen ihre Voraussetzungen erfüllen würden. Wenn Sie eine Reihe von Prädikaten definieren, die Ihnen bestimmte Eigenschaften einer Struktur mitteilen, werden diese Aussagen viel besser lesbar.

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

Es werden auch nützlichere Fehlermeldungen angezeigt.

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

hilft viel mehr als sagen

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

Wo müsste man sich zuerst den Quellcode im Kontext ansehen, um herauszufinden, was tatsächlich getestet wurde.

Wenn Sie eine classmit nicht-trivialen Invarianten haben, ist es wahrscheinlich eine gute Idee, diese von Zeit zu Zeit zu bestätigen, wenn Sie mit dem internen Status in Konflikt geraten sind und sicherstellen möchten, dass Sie das Objekt bei der Rückkehr in einem gültigen Status belassen.

Zu diesem Zweck fand ich es nützlich, eine privateMember-Funktion zu definieren , die ich herkömmlicherweise aufrufe class_invaraiants_hold_. Angenommen std::vector, Sie haben eine Neuimplementierung durchgeführt (da wir alle wissen, dass sie nicht gut genug ist), könnte sie eine Funktion wie diese haben.

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;
}

Beachten Sie ein paar Dinge dazu.

  • Die Prädikatfunktion selbst ist constund noexceptgemäß der Richtlinie, dass Aussagen keine Nebenwirkungen haben dürfen. Wenn es sinnvoll ist, erklären Sie es auch constexpr.
  • Das Prädikat selbst behauptet nichts. Es soll innerhalb von Behauptungen aufgerufen werden , wie z assert(this->class_invariants_hold_()). Auf diese Weise können wir sicher sein, dass bei der Kompilierung von Zusicherungen kein Laufzeit-Overhead anfällt.
  • Der Kontrollfluss innerhalb der Funktion ist in mehrere ifAnweisungen mit einem frühen returns und keinem großen Ausdruck unterteilt. Dies macht es einfach, die Funktion in einem Debugger zu durchlaufen und herauszufinden, welcher Teil der Invariante beschädigt war, wenn die Behauptung ausgelöst wurde.

Behaupte nicht dumme Dinge

Manche Dinge machen einfach keinen Sinn, sich zu behaupten.

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

Diese Behauptungen machen den Code nicht einmal ein bisschen lesbarer oder einfacher zu überlegen. Jeder C ++ - Programmierer sollte sicher genug sein, wie es std::vectorfunktioniert, um sicherzugehen, dass der obige Code korrekt ist, wenn er nur angeschaut wird. Ich sage nicht, dass Sie sich niemals auf die Größe eines Containers festlegen sollten. Wenn Sie Elemente mithilfe eines nicht trivialen Kontrollflusses hinzugefügt oder entfernt haben, kann eine solche Behauptung hilfreich sein. Wenn jedoch nur wiederholt wird, was in dem oben beschriebenen Nicht-Assertions-Code geschrieben wurde, wird kein Wert gewonnen.

Stellen Sie auch nicht sicher, dass die Bibliotheksfunktionen korrekt funktionieren.

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

Wenn Sie der Bibliothek so wenig vertrauen, sollten Sie stattdessen eine andere Bibliothek verwenden.

Wenn andererseits die Dokumentation der Bibliothek nicht 100% eindeutig ist und Sie durch Lesen des Quellcodes Vertrauen in ihre Verträge gewinnen, ist es sehr sinnvoll, auf diesen „abgeleiteten Vertrag“ zu vertrauen. Wenn es in einer zukünftigen Version der Bibliothek kaputt geht, werden Sie es schnell bemerken.

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());

Dies ist besser als die folgende Lösung, die Ihnen nicht sagt, ob Ihre Annahmen korrekt waren.

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();
  }

Behauptungen nicht missbrauchen, um Programmlogik zu implementieren

Behauptungen sollten immer nur verwendet werden, um Fehler aufzudecken , die es wert sind, Ihre Anwendung sofort zu beenden. Sie sollten nicht verwendet werden, um andere Zustände zu verifizieren, selbst wenn die entsprechende Reaktion auf diesen Zustand auch darin bestehen würde, sofort zu beenden.

Schreiben Sie deshalb folgendes…

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

…stattdessen.

assert(server_reachable());

Auch niemals Behauptungen verwenden , um nicht vertrauenswürdige Eingaben zu bestätigen oder überprüfen, ob std::malloctat nicht returnSie die nullptr. Selbst wenn Sie wissen, dass Sie Behauptungen selbst in Release-Builds niemals deaktivieren werden, teilt eine Behauptung dem Leser mit, dass sie etwas überprüft, das immer wahr ist, vorausgesetzt, das Programm ist fehlerfrei und hat keine sichtbaren Nebenwirkungen. Wenn dies nicht die Art von Nachricht ist, die Sie kommunizieren möchten, verwenden Sie einen alternativen Fehlerbehandlungsmechanismus, z. B. throweine Ausnahme. Wenn Sie es bequem finden, einen Makro-Wrapper für Ihre Nicht-Assertions-Checks zu haben, schreiben Sie einen. Nennen Sie es einfach nicht "behaupten", "annehmen", "verlangen", "sicherstellen" oder so ähnlich. Ihre interne Logik könnte dieselbe sein wie für assert, außer dass sie natürlich niemals kompiliert wird.

Mehr Informationen

Ich fand die Rede John Lakos Defensive Programmierung Done Right , da bei CppCon'14 ( 1 st Teil , 2 nd Teil sehr) erleuchtet. Er hat die Idee, die aktivierten Behauptungen anzupassen und auf fehlgeschlagene Ausnahmen noch weiter zu reagieren als in dieser Antwort.

5gon12eder
quelle
4
Assertions are great, but ... you will turn them off sooner or later.- Hoffentlich früher, wie vor dem Versand des Codes. Dinge, die das Programm in der Produktion zum Sterben bringen müssen, sollten Teil des "echten" Codes sein, nicht in Behauptungen.
Blrfl
4

Ich finde, dass ich im Laufe der Zeit weniger Asserts schreibe, weil viele von ihnen "der Compiler arbeitet" und "die Bibliothek arbeitet" bedeuten. Wenn Sie erst einmal darüber nachdenken, was genau Sie testen, werden Sie vermutlich weniger Asserts schreiben.

Zum Beispiel sollte eine Methode, die einer Sammlung etwas hinzufügt, nicht behaupten müssen, dass die Sammlung vorhanden ist - dies ist im Allgemeinen entweder eine Voraussetzung für die Klasse, der die Nachricht gehört, oder ein schwerwiegender Fehler, der sie an den Benutzer zurückgeben sollte . Überprüfen Sie es also einmal, sehr früh, und nehmen Sie es dann an.

Behauptungen sind für mich ein Debugging-Tool, und ich werde sie im Allgemeinen auf zwei Arten verwenden: Auffinden eines Fehlers an meinem Schreibtisch (und sie werden nicht eingecheckt. Nun, vielleicht der eine Schlüssel, den man einchecken könnte); und einen Fehler auf dem Schreibtisch des Kunden zu finden (und sie werden eingecheckt). In beiden Fällen verwende ich Assertions hauptsächlich, um einen Stack-Trace zu generieren, nachdem eine Ausnahme so früh wie möglich erzwungen wurde. Beachten Sie, dass Assertions, die auf diese Weise verwendet werden, leicht zu Heisenbugs führen können - der Fehler kann durchaus nie in der Debug-Erstellung auftreten, für die Assertions aktiviert sind.


quelle
4
Ich verstehe Ihren Standpunkt nicht, wenn Sie sagen: „Dies ist im Allgemeinen entweder eine Voraussetzung für die Klasse, der die Nachricht gehört, oder es ist ein schwerwiegender Fehler, der dazu führen sollte, dass die Nachricht an den Benutzer zurückgegeben wird. Prüfen Sie es also einmal, sehr früh, und nehmen Sie es dann an. “ Wofür verwenden Sie Behauptungen, wenn nicht, um Ihre Annahmen zu überprüfen?
5gon12eder
4

Zu wenige Behauptungen: Viel Glück beim Ändern dieses Codes, der mit versteckten Annahmen behaftet ist.

Zu viele Behauptungen: Kann zu Lesbarkeitsproblemen und möglicherweise zu Codegerüchen führen. Wurden Klasse, Funktion und API richtig entwickelt, wenn so viele Annahmen in Behauptungsanweisungen enthalten sind?

Es könnte auch Assertions geben, die in jeder Funktion nichts wirklich überprüfen oder Dinge wie Compilereinstellungen überprüfen: /

Streben Sie den Sweet Spot an, aber nicht weniger (wie bereits jemand anderes gesagt hat, ist "mehr" von Behauptungen weniger schädlich als wenn zu wenige oder Gott uns helfen - keine).

Beschädigen
quelle
3

Es wäre fantastisch, wenn Sie eine Assert-Funktion schreiben könnten, die nur auf eine boolesche CONST-Methode verweist. Auf diese Weise können Sie sicher sein, dass Ihre Asserts keine Nebenwirkungen haben, indem Sie sicherstellen, dass zum Testen der Assert-Methode eine boolesche const-Methode verwendet wird

Es würde ein wenig von der Lesbarkeit abhängen, zumal ich nicht glaube, dass Sie ein Lambda (in c ++ 0x) nicht mit Anmerkungen versehen können, um eine Konstante für eine Klasse zu sein, was bedeutet, dass Sie dafür keine Lambdas verwenden können

Overkill, wenn Sie mich fragen, aber wenn ich aufgrund von Behauptungen anfangen würde, ein gewisses Maß an Verschmutzung zu sehen, wäre ich in Bezug auf zwei Dinge vorsichtig:

  • Sicherstellen, dass keine Nebenwirkungen in der Assertion auftreten (bereitgestellt durch ein Konstrukt wie oben erläutert)
  • Leistung während der Entwicklungstests; Dies kann durch Hinzufügen von Ebenen (wie z. B. Protokollierung) zur Assert-Funktion behoben werden. Sie können also einige Asserts aus einem Entwicklungsbuild deaktivieren, um die Leistung zu verbessern
lurscher
quelle
2
Heiliger Mist, du magst das Wort "bestimmt" und seine Ableitungen. Ich zähle 8 Verwendungen.
Casey Patton
Ja, tut mir leid, ich neige dazu, Worte viel zu sehr zu
klirren
2

Ich habe viel mehr in C # geschrieben als in C ++, aber die beiden Sprachen sind nicht sehr weit voneinander entfernt. In .Net verwende ich Asserts für Bedingungen, die nicht auftreten sollten, aber ich mache auch oft Ausnahmen, wenn es keine Möglichkeit gibt, fortzufahren. Der VS2010-Debugger zeigt mir viele gute Informationen zu einer Ausnahme, egal wie optimiert der Release-Build ist. Es ist auch eine gute Idee, Unit-Tests hinzuzufügen, wenn Sie können. Manchmal ist die Protokollierung auch eine gute Debug-Hilfe.

Kann es also zu viele Behauptungen geben? Ja. Die Wahl zwischen Abbrechen / Ignorieren / Fortsetzen 15 Mal in einer Minute wird ärgerlich. Eine Ausnahme wird nur einmal ausgelöst. Es ist schwer zu quantifizieren, an welchem ​​Punkt zu viele Zusicherungen vorliegen, aber wenn Ihre Zusicherungen die Rolle von Zusicherungen, Ausnahmen, Komponententests und Protokollierung erfüllen, stimmt etwas nicht.

Ich würde Behauptungen für die Szenarien behalten, die nicht passieren sollten. Sie können anfangs zu viel behaupten, weil Zusicherungen schneller zu schreiben sind, aber den Code später neu faktorisieren - einige davon in Ausnahmen, andere in Tests usw. verwandeln Kommentar neben jedem, den Sie überarbeiten möchten, und NICHT VERGESSEN, das TODO später anzusprechen.

Job
quelle
Wenn Ihr Code 15 Zusicherungen pro Minute nicht erfüllt, liegt meines Erachtens ein größeres Problem vor. Behauptungen sollten niemals in fehlerfreiem Code ausgelöst werden. Sie sollten die Anwendung beenden, um weiteren Schaden zu verhindern, oder Sie sollten in einen Debugger versetzt werden, um zu sehen, was vor sich geht.
5gon12eder
2

Ich möchte mit dir arbeiten! Jemand, der viel schreibt, assertsist fantastisch. Ich weiß nicht, ob es so etwas wie "zu viele" gibt. Weitaus häufiger sind für mich Leute, die zu wenig schreiben und letztendlich auf die gelegentliche tödliche UB-Ausgabe stoßen, die nur auf einem Vollmond auftaucht, der sich mit einem simplen leicht wiederholt hätte reproduzieren lassen assert.

Fehlermeldung

Die eine Sache, an die ich denken kann, ist, Fehlerinformationen in die einzubetten, assertwenn Sie es nicht bereits tun, wie folgt :

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

Auf diese Weise haben Sie möglicherweise nicht mehr das Gefühl, zu viele zu haben, wenn Sie dies nicht bereits getan haben, da Sie jetzt Ihre Aussagen dazu bringen, eine stärkere Rolle bei der Dokumentation von Annahmen und Voraussetzungen zu spielen.

Nebenwirkungen

Natürlich assertkann man das auch tatsächlich missbrauchen und so Fehler einbringen:

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

... wenn es zu foo()Nebenwirkungen kommt, sollten Sie diesbezüglich sehr vorsichtig sein, aber ich bin sicher, Sie sind bereits einer, der sehr liberal behauptet (ein "erfahrener Asserter"). Hoffentlich ist Ihr Testverfahren auch so gut wie Ihre sorgfältige Beachtung von Annahmen.

Debugging-Geschwindigkeit

Während die Geschwindigkeit des Debuggens im Allgemeinen ganz unten auf unserer Prioritätenliste stehen sollte, habe ich einmal so viel in einer Codebasis behauptet, bevor der Debug-Build über den Debugger mehr als 100-mal langsamer als die Veröffentlichung war.

Das lag hauptsächlich daran, dass ich Funktionen wie diese hatte:

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]
    );
}

... bei dem jeder einzelne Anruf zu operator[]einer Überprüfung der Grenzen führen würde. Am Ende ersetzte ich einige der leistungskritischen durch unsichere Entsprechungen, die nicht nur das Debugging-Build drastisch beschleunigen, sondern auch die Sicherheit auf Implementierungsdetailebene beeinträchtigen und nur deshalb, weil der Geschwindigkeitsschub einsetzte Dies führt zu einer spürbaren Produktivitätsverschlechterung (der Vorteil eines schnelleren Debuggens überwiegt die Kosten für den Verlust einiger weniger Asserts, jedoch nur für Funktionen wie diese produktübergreifende Funktion, die auf den kritischsten, gemessenen Pfaden verwendet wurde, nicht für operator[]im Allgemeinen).

Grundsatz der einheitlichen Verantwortung

Ich glaube zwar nicht, dass Sie mit mehr Behauptungen wirklich etwas falsch machen können (zumindest ist es weitaus besser, sich auf die Seite von zu vielen als auf die von zu wenigen zu setzen), aber die Behauptungen selbst sind möglicherweise kein Problem, sondern weisen möglicherweise auf eine hin.

Wenn Sie beispielsweise 5 Zusicherungen für einen einzelnen Funktionsaufruf haben, kann dies zu viel bewirken. Die Schnittstelle hat möglicherweise zu viele Vorbedingungen und Eingabeparameter. Ich halte dies beispielsweise nicht für das Thema, aus dem sich eine gesunde Anzahl von Behauptungen zusammensetzt (auf die ich im Allgemeinen antworten würde: "Je mehr, desto besser!"), Aber das könnte sein eine mögliche rote Fahne (oder sehr wahrscheinlich nicht).


quelle
1
Nun, theoretisch kann es "zu viele" Asserts geben, obwohl dieses Problem sehr schnell offensichtlich wird: Wenn die Assertion erheblich länger dauert als das Fleisch der Funktion. Zugegeben, ich kann mich nicht erinnern, festgestellt zu haben, dass in freier Wildbahn das gegenteilige Problem vorherrscht.
Deduplikator
@ Deduplicator Ah ja, ich bin auf diesen Fall in diesen kritischen Vektor-Mathematik-Routinen gestoßen. Obwohl es definitiv viel besser ist, sich auf zu viele als auf zu wenige zu beschränken!
-1

Es ist sehr vernünftig, Ihrem Code Überprüfungen hinzuzufügen. Für einfache Zusicherungen (die in C und C ++ Compiler integriert sind) ist mein Verwendungsmuster, dass eine fehlgeschlagene Zusicherung bedeutet, dass es einen Fehler im Code gibt, der behoben werden muss. Ich interpretiere das ein bisschen großzügig; Wenn ich erwarte, dass eine Webanforderung den Status 200 zurückgibt und ohne Bearbeitung anderer Fälle darauf hinweist, zeigt eine fehlgeschlagene Zusicherung tatsächlich einen Fehler in meinem Code, sodass die Zusicherung gerechtfertigt ist.

Wenn also Leute behaupten, dass nur überprüft wird, was der Code tut, ist das überflüssig, was nicht ganz richtig ist. Diese Zusicherung überprüft, was der Code ihrer Meinung nach tut, und der Sinn der Zusicherung besteht darin, zu überprüfen, ob die Annahme, dass kein Fehler im Code vorliegt, richtig ist. Und die Behauptung kann auch als Dokumentation dienen. Wenn ich davon ausgehe, dass nach dem Ausführen einer Schleife i == n und es nicht zu 100% aus dem Code ersichtlich ist, ist "assert (i == n)" hilfreich.

Es ist besser, mehr als nur "Behauptung" in Ihrem Repertoire zu haben, um mit unterschiedlichen Situationen umzugehen. Zum Beispiel die Situation, in der ich überprüfe, dass etwas nicht passiert, was auf einen Fehler hindeutet, aber trotzdem weiter daran arbeite. (Wenn ich beispielsweise einen Cache verwende, prüfe ich möglicherweise, ob Fehler vorliegen. Wenn ein Fehler unerwartet auftritt, kann er möglicherweise sicher behoben werden, indem der Cache weggeworfen wird. Ich möchte etwas, das beinahe eine Bestätigung darstellt und mir während der Entwicklung mitteilt und lass mich trotzdem weitermachen.

Ein anderes Beispiel ist die Situation, in der ich nicht erwarte, dass etwas passiert. Ich habe eine allgemeine Problemumgehung, aber wenn dies passiert, möchte ich etwas darüber wissen und es untersuchen. Wieder so etwas wie eine Behauptung, die mir während der Entwicklung sagen sollte. Aber nicht ganz eine Behauptung.

Zu viele Asserts: Wenn ein Assert Ihr Programm zum Absturz bringt, während es sich in den Händen des Benutzers befindet, dürfen Sie keine Asserts haben, die wegen falscher Negative abstürzen.

gnasher729
quelle
-3

Es hängt davon ab, ob. Wenn die Codeanforderungen eindeutig dokumentiert sind, sollte die Zusicherung immer mit den Anforderungen übereinstimmen. In diesem Fall ist es eine gute Sache. Wenn es jedoch keine oder schlecht geschriebene Anforderungen gibt, ist es für neue Programmierer schwierig, Code zu bearbeiten, ohne jedes Mal den Komponententest durchlaufen zu müssen, um die Anforderungen zu ermitteln.

Cucky Arabi
quelle
3
dies scheint nicht zu bieten alles wesentliche über gemacht Punkte und erläuterte vor 8 Antworten
gnat