È possibile scrivere troppi asserti?


33

Sono un grande fan della scrittura di assertassegni nel codice C ++ come un modo per rilevare casi durante lo sviluppo che non possono accadere ma accadono a causa di errori logici nel mio programma. Questa è una buona pratica in generale.

Tuttavia, ho notato che alcune funzioni che scrivo (che fanno parte di una classe complessa) hanno 5+ asserzioni che sembrano potenzialmente una cattiva pratica di programmazione, in termini di leggibilità e manutenibilità. Penso che sia ancora eccezionale, poiché ognuno mi richiede di pensare alle condizioni pre e post delle funzioni e aiutano davvero a catturare i bug. Tuttavia, volevo solo esporlo per chiedere se esistessero paradigmi migliori per rilevare errori logici nei casi in cui fosse necessario un gran numero di controlli.

Commento di Emacs : dal momento che Emacs è il mio IDE preferito, ho leggermente oscurato le dichiarazioni di asserzione che aiutano a ridurre la sensazione di confusione che possono fornire. Ecco cosa aggiungo al mio file .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
Devo ammettere che questa è una domanda che mi è passata per la testa di tanto in tanto. Interessato a sentire le opinioni degli altri su questo.
Captain Sensible,

Risposte:


45

Ho visto centinaia di bug che sarebbero stati risolti più velocemente se qualcuno avesse scritto più affermazioni, e non uno solo che sarebbe stato risolto più rapidamente scrivendo meno .

[C] potrebbe [troppe affermazioni] potenzialmente essere una cattiva pratica di programmazione, in termini di leggibilità e manutenibilità [?]

La leggibilità potrebbe essere un problema, forse - anche se è stata la mia esperienza che le persone che scrivono buone affermazioni scrivono anche codice leggibile. E non mi disturba mai vedere l'inizio di una funzione che inizia con un blocco di assert per verificare che gli argomenti non siano spazzatura: basta inserire una riga vuota dopo di essa.

Anche nella mia esperienza, la manutenibilità è sempre migliorata dalle affermazioni, così come dai test unitari. Gli assert forniscono un controllo di integrità che il codice viene utilizzato nel modo in cui era previsto.


1
Buona risposta. Ho anche aggiunto una descrizione alla domanda su come migliorare la leggibilità con Emacs.
Alan Turing,

2
"È stata la mia esperienza che le persone che scrivono buone affermazioni scrivono anche un codice leggibile" << punto eccellente. Rendere il codice leggibile dipende dal singolo programmatore così come lo sono le tecniche che lui e lei sono e non possono usare. Ho visto le buone tecniche diventare illeggibili nelle mani sbagliate e persino ciò che la maggior parte considererebbe cattive tecniche diventa perfettamente chiaro, persino elegante, con l'uso corretto di astrazione e commenti.
Greg Jackson,

Ho avuto alcuni crash dell'applicazione causati da asserzioni errate. Quindi ho visto bug che non sarebbero esistiti se qualcuno (me stesso) avesse scritto meno affermazioni.
CodesInChaos

@CodesInChaos Probabilmente, errori di battitura a parte, questo indica un errore nella formulazione del problema - ovvero, il bug era nella progettazione, quindi la discrepanza tra asserzioni e (altro) codice.
Lawrence,

12

È possibile scrivere troppi asserti?

Certamente. [Immagina un esempio odioso qui.] Tuttavia, applicando le linee guida dettagliate di seguito, non dovresti avere problemi a spingere quel limite in pratica. Sono anche un grande fan delle affermazioni e le uso secondo questi principi. Gran parte di questo consiglio non è speciale per le asserzioni, ma solo una buona pratica ingegneristica generale applicata ad esse.

Tieni a mente il tempo di esecuzione e il footprint binario

Le asserzioni sono ottime, ma se rendono il programma inaccettabilmente lento, sarà o molto fastidioso o le spegnerai prima o poi.

Mi piace valutare il costo di un'asserzione rispetto al costo della funzione in cui è contenuta. Considera i seguenti due esempi.

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

La stessa funzione è un'operazione O (1) ma le asserzioni spiegano O ( n spese generali ). Non credo che vorresti che tali controlli fossero attivi se non in circostanze molto speciali.

Ecco un'altra funzione con asserzioni simili.

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

La stessa funzione è un'operazione O ( n ), quindi fa molto meno per aggiungere un'ulteriore O ( n overhead ) per l'asserzione. Rallentare una funzione di un piccolo fattore costante (in questo caso, probabilmente inferiore a 3) è qualcosa che di solito possiamo permetterci in una build di debug ma forse non in una build di rilascio.

Ora considera questo esempio.

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

Mentre molte persone probabilmente saranno molto più a loro agio con questa affermazione O (1) che con le due O ( n asserzioni ) nell'esempio precedente, secondo me sono moralmente equivalenti. Ognuno aggiunge overhead sull'ordine della complessità della funzione stessa.

Infine, ci sono le affermazioni "davvero economiche" che sono dominate dalla complessità della funzione in cui sono contenute.

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

Qui, abbiamo due asserzioni O (1) in una funzione O ( n ). Probabilmente non sarà un problema mantenere questo sovraccarico anche nelle build di rilascio.

Tieni presente, tuttavia, che le complessità asintotiche non sempre danno una stima adeguata perché in pratica, abbiamo sempre a che fare con dimensioni di input limitate da alcuni fattori e costanti finiti nascosti da "Big- O " potrebbe non essere trascurabile.

Quindi ora abbiamo identificato diversi scenari, cosa possiamo fare al riguardo? Un approccio (probabilmente troppo semplice) sarebbe quello di seguire una regola come "Non usare asserzioni che dominano la funzione in cui sono contenute". Mentre potrebbe funzionare per alcuni progetti, altri potrebbero aver bisogno di un approccio più differenziato. Ciò potrebbe essere fatto utilizzando diverse macro di asserzioni per i diversi casi.

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

}

Ora puoi usare le tre macro MY_ASSERT_LOW, MY_ASSERT_MEDIUMe MY_ASSERT_HIGHinvece della macro "one size size all" della libreria standard assertper affermazioni che sono dominate, né dominate né dominanti e dominanti la complessità della loro funzione di contenimento, rispettivamente. Quando si crea il software, è possibile pre-definire il simbolo del pre-processore MY_ASSERT_COST_LIMITper selezionare quale tipo di asserzioni dovrebbe trasformarlo nell'eseguibile. Le costanti MY_ASSERT_COST_NONEe MY_ASSERT_COST_ALLnon corrispondono a nessuna macro di asserzione e sono pensate per essere utilizzate come valori MY_ASSERT_COST_LIMITal fine di attivare o disattivare tutte le asserzioni rispettivamente.

Facciamo affidamento sul presupposto che un buon compilatore non genererà alcun codice per

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

e trasformare

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

in

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

che credo sia un presupposto sicuro al giorno d'oggi.

Se stai per modificare il codice sopra, prendi in considerazione le annotazioni specifiche del compilatore come __attribute__ ((cold))on my::assertion_failedo __builtin_expect(…, false)on !(CONDITION)per ridurre il sovraccarico delle asserzioni passate. Nelle build di rilascio, puoi anche considerare di sostituire la chiamata di funzione my::assertion_failedcon qualcosa di simile __builtin_trapper ridurre l'impronta digitale nell'inconveniente di perdere un messaggio diagnostico.

Questi tipi di ottimizzazioni sono realmente rilevanti solo in asserzioni estremamente economiche (come il confronto di due numeri interi che sono già indicati come argomenti) in una funzione che è di per sé molto compatta, non considerando la dimensione aggiuntiva del binario accumulato incorporando tutte le stringhe di messaggio.

Confronta come questo codice

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

viene compilato nel seguente assieme

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

mentre il seguente codice

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

dà questo assemblaggio

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

con cui mi sento molto più a mio agio. (Gli esempi sono stati testati con GCC 5.3.0 usando il -std=c++14, -O3e -march=nativeflag su 4.3.3-2-ARCH x86_64 GNU / Linux. Non mostrati nei frammenti di cui sopra sono le dichiarazioni di test::positive_difference_1ste con test::positive_difference_2ndcui ho aggiunto il __attribute__ ((hot)). È my::assertion_failedstato dichiarato __attribute__ ((cold)).)

Asserire i presupposti nella funzione che dipende da essi

Supponiamo di avere la seguente funzione con il contratto specificato.

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

Invece di scrivere

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

in ciascun sito di chiamata, inserire quella logica una volta nella definizione di 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;
}

e chiamalo senza ulteriori indugi.

const auto frequency = count_letters(text, letter);

Questo ha i seguenti vantaggi.

  • Devi solo scrivere il codice di asserzione una volta. Poiché lo scopo stesso delle funzioni è che vengono chiamate, spesso più di una volta, ciò dovrebbe ridurre il numero complessivo di assertistruzioni nel codice.
  • Mantiene la logica che controlla i presupposti vicino alla logica che dipende da essi. Penso che questo sia l'aspetto più importante. Se i tuoi clienti abusano della tua interfaccia, non si può presumere che applichino correttamente le asserzioni, quindi è meglio che la funzione gli dica.

L'ovvio svantaggio è che non verrà visualizzata la posizione di origine del sito di chiamata nel messaggio di diagnostica. Credo che questo sia un problema minore. Un buon debugger dovrebbe essere in grado di farti risalire all'origine della violazione del contratto in modo conveniente.

Lo stesso pensiero si applica a funzioni "speciali" come gli operatori sovraccarichi. Quando scrivo iteratori, di solito, se la natura dell'iteratore lo consente, conferisco loro una funzione membro

bool
good() const noexcept;

ciò consente di chiedere se è sicuro di dereferenziare l'iteratore. (Naturalmente, in pratica, è quasi sempre possibile solo garantire che non sarà sicuro dereferenziare l'iteratore. Ma credo che tu possa ancora catturare molti bug con una tale funzione.) Invece di sporcare tutto il mio codice che utilizza l'iteratore con le assert(iter.good())dichiarazioni, preferirei mettere un singolo assert(this->good())come prima riga dell'implementazione operator*dell'iteratore.

Se stai usando la libreria standard, invece di affermare manualmente le sue precondizioni nel tuo codice sorgente, attiva i loro controlli nelle build di debug. Possono eseguire controlli ancora più sofisticati come verificare se esiste ancora il contenitore a cui fa riferimento un iteratore. (Vedi la documentazione per libstdc ++ e libc ++ (work in progress) per maggiori informazioni.)

Fattore condizioni comuni fuori

Supponiamo di scrivere un pacchetto di algebra lineare. Molte funzioni avranno precondizioni complicate e la loro violazione spesso causerà risultati errati che non sono immediatamente riconoscibili come tali. Sarebbe molto utile se queste funzioni affermassero i loro presupposti. Se si definiscono un insieme di predicati che indicano determinate proprietà di una struttura, tali asserzioni diventano molto più leggibili.

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

Fornirà anche messaggi di errore più utili.

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

aiuta molto di più di, diciamo

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

dove dovresti prima andare a guardare il codice sorgente nel contesto per capire cosa è stato effettivamente testato.

Se hai a classcon invarianti non banali, è probabilmente una buona idea far valere di tanto in tanto quando hai incasinato lo stato interno e vuoi assicurarti di lasciare l'oggetto in uno stato valido al ritorno.

A tale scopo, ho trovato utile definire una privatefunzione membro che chiamo convenzionalmente class_invaraiants_hold_. Supponiamo che tu std::vectorstia implementando nuovamente (perché sappiamo tutti che non è abbastanza buono.), Potrebbe avere una funzione come questa.

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

Nota alcune cose a riguardo.

  • La stessa funzione predicata è conste noexcept, conformemente alla linea guida, che le asserzioni non devono avere effetti collaterali. Se ha senso, dichiaralo anche tu constexpr.
  • Il predicato non afferma nulla in sé. È pensato per essere chiamato all'interno di asserzioni, come assert(this->class_invariants_hold_()). In questo modo, se le asserzioni vengono compilate, possiamo essere certi che non si verifichino costi generali di runtime.
  • Il flusso di controllo all'interno della funzione è suddiviso in più ifistruzioni con le prime returns anziché un'espressione di grandi dimensioni. Ciò semplifica il passaggio attraverso la funzione in un debugger e consente di scoprire quale parte dell'invariante è stata interrotta in caso di attivazione dell'asserzione.

Non far valere cose sciocche

Alcune cose non hanno senso affermare.

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

Queste affermazioni non rendono il codice un po 'più leggibile o più facile da ragionare. Ogni programmatore C ++ dovrebbe essere abbastanza sicuro di come std::vectorfunziona per essere sicuro che il codice sopra riportato sia corretto semplicemente guardandolo. Non sto dicendo che non si dovrebbe mai far valere le dimensioni di un contenitore. Se hai aggiunto o rimosso elementi usando un flusso di controllo non banale, una simile affermazione può essere utile. Ma se si ripete semplicemente ciò che è stato scritto nel codice di asserzione appena sopra, non si ottiene alcun valore.

Inoltre, non affermare che le funzioni della libreria funzionino correttamente.

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

Se ti fidi della libreria così poco, è meglio considerare di utilizzare un'altra libreria.

D'altra parte, se la documentazione della biblioteca non è chiara al 100% e si acquisisce fiducia sui suoi contratti leggendo il codice sorgente, ha molto senso affermare quel "contratto inferito". Se viene rotto in una versione futura della libreria, noterai rapidamente.

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

Questo è meglio della seguente soluzione che non ti dirà se i tuoi presupposti erano corretti.

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

Non abusare delle affermazioni per implementare la logica del programma

Le asserzioni dovrebbero essere usate sempre e solo per scoprire bug degni di uccidere immediatamente l'applicazione. Non dovrebbero essere utilizzati per verificare qualsiasi altra condizione anche se la reazione appropriata a tale condizione sarebbe anche quella di uscire immediatamente.

Pertanto, scrivi questo ...

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

…invece di quello.

assert(server_reachable());

Inoltre non usare mai le asserzioni per convalidare l'input non attendibile o verificare che std::mallocinvece no returnla nullptr. Anche se sai che non disattiverai mai le asserzioni, anche nelle build di rilascio, un'asserzione comunica al lettore che controlla qualcosa che è sempre vero dato che il programma è privo di bug e non ha effetti collaterali visibili. Se questo non è il tipo di messaggio che si desidera comunicare, utilizzare un meccanismo alternativo di gestione degli errori, come ad esempio throwun'eccezione. Se ritieni conveniente disporre di un wrapper di macro per i controlli di asserzione, procedi scrivendone uno. Basta non chiamarlo "asserire", "assumere", "richiedere", "garantire" o qualcosa del genere. La sua logica interna potrebbe essere la stessa di assert, tranne per il fatto che non è mai stata compilata, ovviamente.

Maggiori informazioni

Ho trovato parlare di Giovanni Lakos' Programmazione difensiva Done Right , dato a CppCon'14 ( 1 ° parte , 2 ° parte ) molto illuminante. Prende l'idea di personalizzare quali affermazioni sono abilitate e come reagire alle eccezioni fallite anche più di quanto abbia fatto in questa risposta.


4
Assertions are great, but ... you will turn them off sooner or later.- Speriamo prima, come prima che il codice venga spedito. Le cose che devono far morire il programma in produzione dovrebbero far parte del codice "reale", non in affermazioni.
Blrfl,

4

Trovo che col tempo scrivo meno affermazioni perché molte di esse equivalgono a "funziona il compilatore" e "funziona la libreria". Una volta che inizi a pensare a cosa stai testando, sospetto che scriverai meno affermazioni.

Ad esempio, un metodo che (diciamo) aggiunge qualcosa a una raccolta non dovrebbe aver bisogno di affermare che la raccolta esiste - questo è generalmente un prerequisito della classe proprietaria del messaggio o è un errore fatale che dovrebbe riportarlo all'utente . Quindi controlla una volta, molto presto, quindi assumilo.

Le affermazioni per me sono uno strumento di debug e generalmente le userò in due modi: trovare un bug sulla mia scrivania (e non vengono verificate. Beh, forse l'unica chiave che si potrebbe); e trovare un bug sulla scrivania del cliente (e vengono registrati). Entrambe le volte sto usando asserzioni principalmente per generare una traccia di stack dopo aver forzato un'eccezione il prima possibile. Tieni presente che le asserzioni utilizzate in questo modo possono facilmente portare a heisenbugs : il bug potrebbe non verificarsi mai nella build di debug che ha le asserzioni abilitate.


4
Non capisco il tuo punto quando dici "che generalmente è un prerequisito della classe che possiede il messaggio o è un errore fatale che dovrebbe tornare all'utente. Quindi controllalo una volta, molto presto, quindi assumilo. ”Per cosa stai usando le asserzioni se non per verificare le tue assunzioni?
5gon12eder

4

Troppe asserzioni: buona fortuna cambiando quel codice pieno di ipotesi nascoste.

Troppe asserzioni: può portare a problemi di leggibilità e potenzialmente a odore di codice - la classe, la funzione, l'API sono progettate proprio quando hanno così tante assunzioni inserite nelle dichiarazioni di asserzione?

Potrebbero esserci anche asserzioni che non controllano nulla o controllano cose come le impostazioni del compilatore in ciascuna funzione: /

Punta al punto giusto, ma non meno (come già detto da qualcun altro, "più" di affermazioni è meno dannoso di avere troppo pochi o dio ci aiuti - nessuno).


3

Sarebbe fantastico se tu potessi scrivere una funzione Assert che ha preso solo un riferimento a un metodo CONST booleano, in questo modo sei certo che i tuoi assert non abbiano effetti collaterali assicurando che un metodo const booleano sia usato per testare l'assert

trarrebbe un po 'di leggibilità, specialmente perché non penso che non si possa annotare un lambda (in c ++ 0x) per essere una const in qualche classe, il che significa che non si può usare lambdas per quello

esagerare se me lo chiedi, ma se iniziassi a vedere un certo livello di inquinamento a causa delle affermazioni, diffiderei di due cose:

  • assicurandosi che non si verifichino effetti collaterali nell'asserzione (fornita da un costrutto come spiegato sopra)
  • prestazioni durante i test di sviluppo; questo può essere risolto aggiungendo livelli (come la registrazione) alla struttura di asserzione; così puoi disabilitare alcuni assert da una build di sviluppo al fine di migliorare le prestazioni

2
Santa merda, ti piace la parola "certo" e le sue derivazioni. Conto 8 usi.
Casey Patton,

sì, scusa, tendo a
crollare

2

Ho scritto in C # molto più di quanto abbia fatto in C ++, ma i due linguaggi non sono terribilmente distanti. In .Net utilizzo Assert per condizioni che non dovrebbero verificarsi, ma spesso lancio eccezioni quando non c'è modo di continuare. Il debugger VS2010 mi mostra molte buone informazioni su un'eccezione, non importa quanto sia ottimizzata la build di Release. È anche una buona idea aggiungere test unitari, se possibile. A volte la registrazione è anche una buona cosa da avere come aiuto per il debug.

Quindi, ci possono essere troppe affermazioni? Sì. Scegliere tra Annulla / Ignora / Continua 15 volte in un minuto diventa fastidioso. Un'eccezione viene generata una sola volta. È difficile quantificare il punto in cui ci sono troppi asserzioni, ma se le asserzioni svolgono il ruolo di asserzioni, eccezioni, test unitari e registrazione, allora qualcosa non va.

Riserverei affermazioni per gli scenari che non dovrebbero accadere. All'inizio potresti affermare in modo eccessivo, perché le asserzioni sono più veloci da scrivere, ma ricodificano il codice in un secondo momento: trasformane alcune in eccezioni, altre in test, ecc. Se hai abbastanza disciplina per ripulire ogni commento TODO, lascia un commenta accanto a ciascuno di quelli che intendi rielaborare e NON DIMENTICARE di rivolgerti a TODO in seguito.


Se il tuo codice fallisce 15 affermazioni al minuto, penso che ci sia un problema più grande in questione. Le asserzioni non dovrebbero mai essere attivate in codice privo di bug e, in caso contrario, dovrebbero uccidere l'applicazione per prevenire ulteriori danni o farti cadere in un debugger per vedere cosa sta succedendo.
5gon12eder

2

Voglio lavorare con te! Qualcuno che scrive molto assertsè fantastico. Non so se ci siano "troppi". Molto più comuni per me sono le persone che scrivono troppo poche e alla fine finiscono per imbattersi nell'occasionale numero mortale di UB che si presenta solo su una luna piena che potrebbe essere facilmente riprodotto ripetutamente con un semplice assert.

Messaggio fallito

L'unica cosa che mi viene in mente è quella di incorporare le informazioni sugli errori nel assertcaso in cui non le stiate già facendo, in questo modo:

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

In questo modo potresti non sentirti più come se ne avessi troppi se non lo avessi già fatto, poiché ora stai ottenendo le tue affermazioni per svolgere un ruolo più forte nel documentare ipotesi e condizioni preliminari.

Effetti collaterali

Ovviamente assertpuò effettivamente essere utilizzato in modo improprio e introdurre errori, in questo modo:

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

... Se foo() innesca effetti collaterali, quindi dovresti stare molto attento, ma sono sicuro che sei già uno che afferma molto liberamente (un "assertore esperto"). Spero che la tua procedura di test sia buona quanto la tua attenzione per asserire ipotesi.

Velocità di debug

Mentre la velocità del debug dovrebbe generalmente essere in fondo al nostro elenco di priorità, una volta ho finito per affermare così tanto in una base di codice prima che l'esecuzione del build di debug attraverso il debugger fosse finita 100 volte più lenta della versione.

Principalmente perché avevo funzioni come questa:

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

... dove ogni singola chiamata a operator[]farebbe un'affermazione di controllo dei limiti. Ho finito per sostituire alcuni di quelli critici per le prestazioni con equivalenti non sicuri che non affermano solo di accelerare drasticamente la build di debug a un costo minore solo per la sicurezza a livello di dettaglio di implementazione, e solo perché stava iniziando il colpo di velocità degradare in modo molto evidente la produttività (il vantaggio di ottenere un debug più veloce supera i costi della perdita di alcune affermazioni, ma solo per funzioni come questa funzione di prodotto incrociato che veniva utilizzata nei percorsi più critici e misurati, non operator[]in generale).

Principio unico di responsabilità

Anche se non penso che tu possa davvero sbagliare con più asserzioni (almeno è molto, molto meglio sbagliare dalla parte di troppe che troppo poche), le asserzioni stesse potrebbero non essere un problema ma potrebbero indicarne una.

Se hai 5 asserzioni come una singola chiamata di funzione, ad esempio, potrebbe fare troppo. La sua interfaccia potrebbe avere troppi presupposti e parametri di input, ad esempio ritengo che non sia correlato solo all'argomento di ciò che costituisce un numero salutare di asserzioni (per le quali generalmente risponderei, "più sono, meglio è!"), Ma potrebbe essere una possibile bandiera rossa (o molto probabilmente no).


1
Bene, in teoria possono esserci "troppi" asserzioni, anche se quel problema diventa evidente molto velocemente: se l'asserzione impiega molto più tempo della carne della funzione. Certo, non ricordo di aver scoperto che allo stato selvatico, tuttavia, il problema opposto è prevalente.
Deduplicatore

@Deduplicatore Ah sì, ho riscontrato quel caso in quelle routine matematiche di vettori critici. Anche se sembra decisamente molto meglio sbagliare dalla parte di troppi che di troppo pochi!

-1

È molto ragionevole aggiungere controlli al codice. Per assert semplice (quello integrato nel compilatore C e C ++) il mio modello di utilizzo è che un'asserzione fallita significa che c'è un bug nel codice che deve essere corretto. Lo interpreto un po 'generosamente; se mi aspetto che una richiesta Web restituisca uno stato 200 e lo asserisca senza gestire altri casi, un'asserzione non riuscita mostra effettivamente un bug nel mio codice, quindi l'affermazione è giustificata.

Quindi, quando le persone dicono che un'affermazione che controlla solo ciò che fa il codice è superflua, non è del tutto giusto. Tale asserzione verifica ciò che pensano che faccia il codice e il punto centrale dell'asserzione è verificare che l'assunzione di nessun bug nel codice sia corretta. E l'asserzione può servire anche come documentazione. Se suppongo che dopo aver eseguito un ciclo i == n e non sia ovvio al 100% dal codice, allora "assert (i == n)" sarà utile.

È meglio avere più di una semplice "affermazione" nel proprio repertorio per gestire situazioni diverse. Ad esempio la situazione in cui controllo che non accada qualcosa che indichi un errore, ma continui comunque a aggirare quella condizione. (Ad esempio, se uso un po 'di cache, potrei verificare la presenza di errori e se un errore si verifica inaspettatamente potrebbe essere sicuro correggere l'errore gettando via la cache. Voglio qualcosa che è quasi un'asserzione, che mi dice durante lo sviluppo e mi consente ancora di continuare.

Un altro esempio è la situazione in cui non mi aspetto che accada qualcosa, ho una soluzione alternativa generica, ma se succede questa cosa, voglio conoscerla ed esaminarla. Ancora una volta qualcosa di simile a un'affermazione, che dovrebbe dirmi durante lo sviluppo. Ma non è proprio un'affermazione.

Troppe asserzioni: se un'asserzione arresta in modo anomalo il tuo programma quando è nelle mani dell'utente, non devi avere asserzioni che si arrestino a causa di falsi negativi.


-3

Dipende. Se i requisiti del codice sono chiaramente documentati, l'affermazione deve sempre corrispondere ai requisiti. Nel qual caso è una buona cosa. Tuttavia, se non ci sono requisiti o requisiti scritti in modo errato, sarebbe difficile per i nuovi programmatori modificare il codice senza dover fare riferimento al test unitario ogni volta per capire quali sono i requisiti.


3
questo non sembra offrire nulla di sostanziale rispetto ai punti formulati e spiegati nelle precedenti 8 risposte
moscerino
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.