Est-il possible d'écrire trop d'assertions?


33

Je suis un grand fan de l'écriture de assertvérifications dans le code C ++ comme moyen d'attraper des cas au cours du développement qui ne peuvent pas se produire, mais se produisent en raison de bogues logiques dans mon programme. C'est une bonne pratique en général.

Cependant, j'ai remarqué que certaines fonctions que j'écris (qui font partie d'une classe complexe) ont plus de 5 assertions, ce qui pourrait éventuellement constituer une mauvaise pratique de programmation, en termes de lisibilité et de maintenabilité. Je pense que c'est quand même génial, car chacun exige que je réfléchisse aux conditions préalables et postérieures aux fonctions et cela aide vraiment à attraper les bugs. Cependant, je voulais simplement formuler ceci pour demander s’il existe un meilleur paradigme pour détecter les erreurs de logique dans les cas où un grand nombre de vérifications est nécessaire.

Commentaire d'Emacs : Etant donné qu'Emacs est mon IDE de choix, je l'ai un peu en gris sur les assertions, ce qui aide à réduire le sentiment d'encombrement qu'elles peuvent donner. Voici ce que j'ajoute à mon fichier .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
Je dois admettre que c’est une question qui m’a traversé l’esprit de temps en temps. Intéressé d'entendre l'opinion des autres à ce sujet.
Captain Sensible

Réponses:


45

J'ai vu des centaines de bugs qui auraient été résolus plus rapidement si quelqu'un avait écrit plus d' assertions, et pas un seul qui aurait été résolu plus rapidement en écrivant moins .

[C] ould [trop d'affirmations] pourrait être une mauvaise pratique de programmation, en termes de lisibilité et de maintenabilité [?]

La lisibilité pourrait être un problème, peut-être - bien que mon expérience me montre que les personnes qui écrivent bien écrivent aussi du code lisible. Et cela ne me dérange jamais de voir le début d'une fonction commencer par un bloc d'assertions pour vérifier que les arguments ne sont pas des ordures - il suffit de mettre une ligne vide après celle-ci.

De plus, selon mon expérience, la maintenabilité est toujours améliorée par les assertions, tout comme par les tests unitaires. Les assertions permettent de vérifier que le code est utilisé de la manière dont il était destiné.


1
Bonne réponse. J'ai également ajouté une description à la question de savoir comment améliorer la lisibilité avec Emacs.
Alan Turing

2
"D'après mon expérience, les rédacteurs de bonnes assertions écrivent aussi du code lisible" << excellent point. Rendre le code lisible est aussi simple que le programmeur, car ce sont les techniques qu'il est et qu'il n'est pas autorisé à utiliser. J'ai vu de bonnes techniques devenir illisibles entre de mauvaises mains, et même ce que la plupart des gens considèrent comme mauvaises peuvent devenir parfaitement claires, voire élégantes, en utilisant correctement l'abstraction et les commentaires.
Greg Jackson

J'ai eu quelques problèmes d'application causés par des affirmations erronées. J'ai donc vu des bugs qui n'auraient pas existé si quelqu'un (moi-même) avait écrit moins d'assertions.
CodesInChaos

@CodesInChaos À part les fautes de frappe, cela indique probablement une erreur dans la formulation du problème - c'est-à-dire que le bogue était dans la conception, d'où le décalage entre les assertions et le code (autre).
Lawrence

12

Est-il possible d'écrire trop d'assertions?

Bien sûr que oui. [Imaginez un exemple odieux ici.] Cependant, en appliquant les directives détaillées ci-dessous, vous ne devriez pas avoir de difficulté à repousser cette limite dans la pratique. Je suis également un grand partisan des assertions et les utilise conformément à ces principes. Une grande partie de ces conseils ne sont pas particuliers aux assertions, mais seulement aux bonnes pratiques générales d'ingénierie qui leur sont appliquées.

Gardez à l'esprit les frais généraux d'exécution et d'empreinte binaire

Les affirmations sont excellentes, mais si elles ralentissent votre programme de manière inacceptable, ce sera soit très agaçant, soit vous les désactiverez tôt ou tard.

J'aime jauger le coût d'une assertion par rapport au coût de la fonction dans laquelle elle est contenue. Considérons les deux exemples suivants.

// 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 fonction elle-même est une opération O (1), mais les assertions prennent en compte le temps système O ( n ). Je ne pense pas que vous souhaitiez que ces contrôles soient actifs sauf dans des circonstances très spéciales.

Voici une autre fonction avec des assertions similaires.

// 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 fonction elle-même est une opération O ( n ), il est donc beaucoup moins difficile d’ajouter une surcharge O ( n ) supplémentaire pour l’assertion. Ralentir une fonction d'un petit facteur constant (dans ce cas, probablement inférieur à 3) est quelque chose que nous pouvons généralement nous permettre dans une version de débogage, mais peut-être pas dans une version finale.

Considérons maintenant cet exemple.

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

Alors que beaucoup de gens seront probablement beaucoup plus à l'aise avec cette affirmation O (1) qu'avec les deux affirmations O ( n ) de l'exemple précédent, elles sont moralement équivalentes à mon avis. Chacun ajoute une surcharge sur l'ordre de la complexité de la fonction elle-même.

Enfin, il y a les affirmations «vraiment bon marché» qui sont dominées par la complexité de la fonction dans laquelle elles sont contenues.

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

Ici, nous avons deux assertions O (1) dans une fonction O ( n ). Ce ne sera probablement pas un problème de conserver ces frais généraux, même dans les versions finales.

Cependant, gardez à l'esprit que les complexités asymptotiques ne donnent pas toujours une estimation adéquate car, dans la pratique, nous traitons toujours avec des tailles d'entrées limitées par des facteurs finis constants et constants cachés par “Big- O ” qui pourraient bien ne pas être négligeables.

Alors maintenant que nous avons identifié différents scénarios, que pouvons-nous faire à leur sujet? Une approche (probablement trop) facile consisterait à suivre une règle telle que «N'utilisez pas les assertions qui dominent la fonction dans laquelle elles sont contenues». Même si cela peut fonctionner pour certains projets, d'autres peuvent nécessiter une approche plus différenciée. Cela pourrait être fait en utilisant différentes macros d'assertion pour les différents cas.

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

}

Vous pouvez maintenant utiliser les trois macros MY_ASSERT_LOW, MY_ASSERT_MEDIUMet MY_ASSERT_HIGHau lieu de la assertmacro "one size fits all" de la bibliothèque standard pour les assertions dominées, ni dominées, ni dominantes et dominant la complexité de leur fonction contenant respectivement. Lorsque vous créez le logiciel, vous pouvez prédéfinir le symbole de pré-processeur MY_ASSERT_COST_LIMITpour sélectionner le type d'assertions à inclure dans l'exécutable. Les constantes MY_ASSERT_COST_NONEet MY_ASSERT_COST_ALLne correspondent pas aux macros assert et sont destinés à être utilisés comme valeurs MY_ASSERT_COST_LIMITafin de mettre toutes les affirmations hors ou sur respectivement.

Nous nous appuyons sur l'hypothèse ici qu'un bon compilateur ne générera aucun code pour

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

et transformer

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

dans

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

que je crois est une hypothèse sûre de nos jours.

Si vous êtes sur le point de modifier le code ci-dessus, envisagez des annotations spécifiques au compilateur, comme __attribute__ ((cold))sur my::assertion_failedou __builtin_expect(…, false)sur, !(CONDITION)pour réduire la surcharge des assertions passées. Dans les versions de version, vous pouvez également envisager de remplacer l'appel de fonction my::assertion_failedpar quelque chose comme __builtin_trappour réduire l'encombrement au lieu de perdre un message de diagnostic.

Ces types d’optimisation ne sont vraiment pertinents que dans des assertions extrêmement bon marché (comme comparer deux entiers déjà donnés en arguments) dans une fonction elle-même très compacte, sans tenir compte de la taille supplémentaire du binaire accumulé en incorporant toutes les chaînes de message.

Comparez comment ce 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;
}

est compilé dans l'assemblage suivant

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

tandis que le code suivant

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

donne cette assemblée

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

avec lequel je me sens beaucoup plus à l'aise. (Exemples ont été testés avec GCC 5.3.0 en utilisant la -std=c++14, -O3et des -march=nativedrapeaux sur 4.3.3-2-ARCH x86_64 GNU / Linux. Ne figurent pas dans les extraits ci - dessus sont les déclarations de test::positive_difference_1stet test::positive_difference_2ndque j'ajouté le __attribute__ ((hot))à. my::assertion_failedA été déclarée avec __attribute__ ((cold)).)

Affirmer les conditions préalables dans la fonction qui en dépend

Supposons que vous ayez la fonction suivante avec le contrat spécifié.

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

Au lieu d'écrire

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

à chaque site d'appel, mettez cette logique une fois dans la définition de 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;
}

et appelez-le sans plus tarder.

const auto frequency = count_letters(text, letter);

Cela présente les avantages suivants.

  • Il vous suffit d'écrire le code d'assertion une fois. Etant donné que le but même des fonctions est qu’elles soient appelées - souvent plus d’une fois - cela devrait réduire le nombre total d’ assertinstructions dans votre code.
  • Il maintient la logique qui vérifie les conditions préalables à proximité de la logique qui en dépend. Je pense que c'est l'aspect le plus important. Si vos clients utilisent mal votre interface, on ne peut pas supposer qu'ils appliquent correctement les assertions. Il est donc préférable que la fonction leur indique.

L'inconvénient évident est que le message de diagnostic ne contient pas l'emplacement source du site de l'appel. Je crois que c'est un problème mineur. Un bon débogueur devrait pouvoir vous permettre de retracer facilement l'origine de la violation du contrat.

La même réflexion s’applique aux fonctions «spéciales» telles que les opérateurs surchargés. Lorsque j'écris des itérateurs, généralement, si la nature de l'itérateur le permet, je leur attribue une fonction membre.

bool
good() const noexcept;

cela permet de demander s'il est prudent de déréférencer l'itérateur. (Bien sûr, dans la pratique, il est presque toujours possible de garantir qu'il ne sera pas prudent de déréférencer l'itérateur. Mais je pense que vous pouvez toujours capturer beaucoup de bogues avec une telle fonction.) Au lieu de tout mon code qui utilise l’itérateur avec des assert(iter.good())déclarations, je préfère mettre un simple assert(this->good())comme première ligne de la mise operator*en œuvre de l’itérateur.

Si vous utilisez la bibliothèque standard, au lieu d'affirmer manuellement ses conditions préalables dans votre code source, activez leurs vérifications dans les versions de débogage. Ils peuvent effectuer des vérifications encore plus sophistiquées, comme de vérifier si le conteneur référencé par un itérateur existe toujours. (Consultez la documentation de libstdc ++ et de libc ++ (travaux en cours) pour plus d'informations.)

Facteur conditions communes sur

Supposons que vous écriviez un paquet d’algèbre linéaire. De nombreuses fonctions auront des conditions préalables compliquées et leur violation entraînera souvent des résultats erronés qui ne sont pas immédiatement reconnaissables en tant que tels. Ce serait très bien si ces fonctions affirmaient leurs conditions préalables. Si vous définissez une série de prédicats qui vous indiquent certaines propriétés d'une structure, ces assertions deviennent beaucoup plus lisibles.

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

Cela donnera aussi plus de messages d'erreur utiles.

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

aide beaucoup plus que, disons

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

où vous devez d’abord consulter le code source dans le contexte pour déterminer ce qui a été réellement testé.

Si vous avez classdes invariants non triviaux, c'est probablement une bonne idée de les appliquer de temps en temps lorsque vous avez modifié l'état interne et que vous voulez vous assurer que l'objet est renvoyé dans un état valide.

Dans ce but, j'ai trouvé utile de définir une privatefonction membre que j'appelle de manière conventionnelle class_invaraiants_hold_. Supposons que vous soyez en train de ré-implémenter std::vector(parce que nous savons tous que ce n'est pas assez bon), cela pourrait avoir une fonction comme celle-ci.

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

Notez quelques petites choses à ce sujet.

  • La fonction prédicat lui - même est constet noexcept, conformément à la directive que les affirmations ne doivent pas avoir des effets secondaires. Si cela a du sens, déclarez-le également constexpr.
  • Le prédicat n'affirme rien en soi. Il est destiné à être appelé à l' intérieur d' assertions, telles que assert(this->class_invariants_hold_()). De cette façon, si les assertions sont compilées, nous pouvons être sûrs que cela ne génère pas de temps système supplémentaire.
  • Le flux de contrôle à l'intérieur de la fonction est divisé en plusieurs ifinstructions avec returns au lieu d'une expression large. Cela facilite l'exploration de la fonction dans un débogueur et la découverte de la partie de l'invariant qui a été cassée si l'assertion est déclenchée.

Ne pas affirmer des bêtises

Certaines choses n’ont tout simplement pas de sens.

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

Ces assertions ne rendent pas le code même un tout petit peu plus lisible ou plus facile à raisonner. Chaque programmeur C ++ doit être suffisamment sûr de la manière dont il std::vectorfonctionne pour s’assurer que le code ci-dessus est correct en le regardant simplement. Je ne dis pas que vous ne devriez jamais affirmer sur la taille d'un conteneur. Si vous avez ajouté ou supprimé des éléments à l'aide d'un flux de contrôle non trivial, une telle assertion peut s'avérer utile. Mais si elle répète simplement ce qui a été écrit dans le code de non-assertion juste au-dessus, aucune valeur ne sera gagnée.

Aussi, n'affirmez pas que les fonctions de la bibliothèque fonctionnent correctement.

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

Si vous ne faites que peu confiance à la bibliothèque, pensez plutôt à utiliser une autre bibliothèque.

D'autre part, si la documentation de la bibliothèque n'est pas claire à 100% et que vous prenez confiance en ses contrats en lisant le code source, il est tout à fait logique d'affirmer ce «contrat inféré». Si cela se produit dans une future version de la bibliothèque, vous le remarquerez rapidement.

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

C'est mieux que la solution suivante qui ne vous dira pas si vos hypothèses étaient correctes.

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

Ne pas abuser des assertions pour implémenter la logique du programme

Les assertions ne doivent être utilisées que pour découvrir les bogues dignes de tuer immédiatement votre application. Ils ne doivent pas être utilisés pour vérifier une autre condition même si la réaction appropriée à cette condition serait également de cesser immédiatement.

Par conséquent, écris ceci…

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

…au lieu de.

assert(server_reachable());

De même, n’utilisez jamais d’assertions pour valider des entrées non fiables ou des vérifications qui std::mallocne returnvous ont pas été attribuées nullptr. Même si vous savez que vous ne désactiverez jamais les assertions, même dans les versions finales, une assertion indique au lecteur qu'elle vérifie quelque chose qui est toujours vrai, car le programme ne contient pas de bogues et n'a pas d'effets secondaires visibles. Si ce n'est pas le type de message que vous souhaitez communiquer, utilisez un autre mécanisme de traitement des erreurs, tel qu'une throwexception. Si vous trouvez pratique de disposer d'un wrapper de macro pour vos vérifications de non-assertion, continuez en écrivant un. N'appelez-le pas simplement «affirmer», «assumer», «exiger», «assurer» ou quelque chose du genre. Sa logique interne pourrait être la même que pour assert, sauf qu'elle n'est jamais compilée, bien sûr.

Plus d'information

J'ai trouvé parler de John Lakos Programmation défensive Fait droit , donné à CppCon'14 ( 1 er partie , 2 ème partie ) très instructif. Il prend l'idée de personnaliser quelles assertions sont activées et comment réagir aux exceptions échouées encore plus loin que dans cette réponse.


4
Assertions are great, but ... you will turn them off sooner or later.- J'espère plus tôt, comme avant l'envoi du code. Les éléments qui doivent faire mourir le programme en production doivent faire partie du "vrai" code, pas des assertions.
Blrfl

4

Je trouve qu'au fil du temps, j'écris moins d'assertions, car bon nombre d'entre elles équivalent à "le compilateur fonctionne-t-il" et "à la bibliothèque fonctionne-t-elle". Une fois que vous commencez à penser à ce que vous testez exactement, je pense que vous rédigerez moins d’affirmations.

Par exemple, une méthode qui ajoute (par exemple) quelque chose à une collection ne devrait pas avoir besoin d'affirmer que la collection existe - il s'agit généralement d'une condition préalable de la classe qui détient le message ou d'une erreur fatale qui devrait être renvoyée à l'utilisateur. . Donc, vérifiez-le une fois, très tôt, puis supposez-le.

Les assertions me sont un outil de débogage, et je les utilise généralement de deux manières: trouver un bogue à mon bureau (et elles ne sont pas vérifiées. Eh bien, peut-être que la clé pourrait l'être); et trouver un bug sur le bureau du client (et ils sont enregistrés). Les deux fois, j'utilise principalement des assertions pour générer une trace de pile après avoir forcé une exception le plus tôt possible. Sachez que les assertions utilisées de cette manière peuvent facilement conduire à heisenbugs - le bogue peut ne jamais se produire dans la version de débogage pour laquelle les assertions sont activées.


4
Je ne comprends pas ce que vous voulez dire lorsque vous dites: «Il s’agit généralement d’une condition préalable de la classe à laquelle appartient le message ou d’une erreur fatale qui devrait revenir à l’utilisateur. Alors, vérifiez-le une fois très tôt, puis supposez-le. » Pourquoi utilisez-vous des assertions, sinon pour vérifier vos hypothèses?
5gon12eder

4

Trop peu d’affirmations: bonne chance pour changer ce code truffé d’hypothèses cachées.

Trop d'assertions: peuvent entraîner des problèmes de lisibilité et potentiellement une odeur de code - la classe, la fonction, l'API sont-ils conçus correctement alors qu'il y a autant d'hypothèses placées dans des déclarations d'assert?

Il peut également y avoir des assertions qui ne vérifient pas vraiment quoi que ce soit ou ne vérifient pas des choses telles que les paramètres du compilateur dans chaque fonction: /

Visez le bon compromis, mais pas moins (comme quelqu'un l'a déjà dit, "plus" d'assertions est moins dommageable que d'avoir trop peu ou l'aide de Dieu, aidez-nous - aucune).


3

Ce serait génial si vous pouviez écrire une fonction Assert qui prenait uniquement une référence à une méthode booléenne CONST. De cette manière, vous êtes certain que vos assertions n'ont pas d'effets secondaires en vous assurant qu'une méthode booléenne const est utilisée pour tester l'assert.

cela attirerait un peu sur la lisibilité, spécialement depuis que je ne pense pas que vous ne pouvez pas annoter un lambda (dans c ++ 0x) pour être un const à une classe, ce qui signifie que vous ne pouvez pas utiliser lambdas pour cela

exagération si vous me demandez, mais si je commençais à voir un certain niveau de pollution en raison d'affirmations, je me méfierais de deux choses:

  • en s'assurant qu'aucun effet secondaire ne se produit dans l'assert (fourni par une construction comme expliqué ci-dessus)
  • performances lors des tests de développement; cela peut être résolu en ajoutant des niveaux (comme la journalisation) à la fonction d'assertion; afin que vous puissiez désactiver certaines assertions d'une version de développement afin d'améliorer les performances

2
Bon sang, vous aimez le mot "certain" et ses dérivés. Je compte 8 utilisations.
Casey Patton

oui, désolé, j'ai tendance à trop
parler de

2

J'ai écrit beaucoup plus en C # qu'en C ++, mais les deux langages ne sont pas si éloignés l'un de l'autre. En .Net, j'utilise des assertions pour des conditions qui ne devraient pas se produire, mais je lève aussi souvent des exceptions lorsqu'il n'y a aucun moyen de continuer. Le débogueur VS2010 me montre plein d’informations utiles sur une exception, quelle que soit l’optimisation de la version Release. C'est également une bonne idée d'ajouter des tests unitaires si vous le pouvez. Parfois, la journalisation est également une bonne chose à utiliser comme aide au débogage.

Alors, peut-il y avoir trop d'affirmations? Oui. Choisir entre Abandonner / Ignorer / Continuer 15 fois en une minute devient ennuyeux. Une exception est levée une seule fois. Il est difficile de quantifier le point où il y a trop d'assertions, mais si vos assertions remplissent le rôle d'assertions, d'exceptions, de tests unitaires et de journalisation, il y a quelque chose qui ne va pas.

Je réserverais des assertions aux scénarios qui ne devraient pas se produire. Vous pouvez sur-affirmer au début, car les assertions sont plus rapides à écrire, mais re-factorisez le code plus tard - transformez certaines d'entre elles en exceptions, d'autres en tests, etc. Si vous avez assez de discipline pour nettoyer chaque commentaire TODO, laissez un commentez à côté de chaque élément que vous prévoyez de retravailler et NE PAS OUBLIER d’adresser le TODO plus tard.


Si votre code échoue à 15 assertions par minute, je pense que le problème est plus grave. Les assertions ne devraient jamais être déclenchées avec un code exempt d'erreurs et si elles le faisaient, elles devraient tuer l'application pour éviter d'autres dommages ou vous laisser tomber dans un débogueur pour voir ce qui se passe.
5gon12eder

2

Je veux travailler avec vous! Quelqu'un qui écrit beaucoup assertsest fantastique. Je ne sais pas s'il y a une chose comme "trop". Beaucoup plus commun pour moi sont les gens qui écrivent trop peu et finissent par rencontrer le problème mortel occasionnel d'UB qui n'apparaît que lors d'une pleine lune qui aurait pu être facilement reproduite à plusieurs reprises avec un simple assert.

Message d'échec

La seule chose à laquelle je peux penser est d'intégrer des informations sur les défaillances dans le assertcas où vous ne le faites pas déjà, comme ceci:

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

De cette façon, vous pourriez ne plus avoir l'impression que vous en avez trop si vous ne le faisiez pas déjà, car vous faites maintenant que vos affirmations jouent un rôle plus important dans la documentation des hypothèses et des conditions préalables.

Effets secondaires

Bien sûr, asserton peut effectivement en abuser et introduire des erreurs, comme ceci:

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

... si foo()déclenche des effets secondaires, vous devez donc être très prudent à ce sujet, mais je suis sûr que vous êtes déjà quelqu'un qui affirme de manière très libérale (un "asserter expérimenté"). J'espère que votre procédure de test est aussi valable que votre attention particulière à l'affirmation d'hypothèses.

Débogage Vitesse

Alors que la vitesse de débogage devrait généralement être au bas de notre liste de priorités, une fois, je me suis retrouvé à affirmer tellement de choses dans une base de code avant que l'exécution de la construction de débogage via le débogueur était plus de 100 fois plus lente que la publication.

C'était principalement parce que j'avais des fonctions comme celle-ci:

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

... où chaque appel à operator[]faire ferait une assertion de vérification des limites. J'ai fini par remplacer certaines de ces performances critiques par des équivalents non sécurisés qui ne prétendaient pas simplement accélérer le développement du débogage à un coût minime pour la sécurité au niveau de la mise en œuvre uniquement, et ce uniquement parce que sa vitesse commençait. dégrader très nettement la productivité (l'avantage d'un débogage plus rapide l'emporte sur le coût de perdre quelques assertions, mais uniquement pour des fonctions telles que cette fonction de produit croisé qui était utilisée dans les chemins les plus critiques et mesurés, mais pas operator[]en général).

Principe de responsabilité unique

Bien que je ne pense pas que vous puissiez vous tromper avec plus d’affirmations (du moins, c’est beaucoup mieux, mais vaut mieux en abuser que trop peu), les affirmations elles-mêmes peuvent ne pas poser de problème, mais en indiquer un.

Par exemple, si vous avez 5 assertions pour un seul appel de fonction, cela peut en faire trop. Son interface peut avoir trop de conditions préalables et de paramètres d’entrée, par exemple, j’estime qu’elle n’est pas liée au seul sujet de ce qui constitue un nombre sain d’affirmations (pour lesquelles je répondrais généralement «plus on est de fous!»), Mais c’est peut-être un drapeau rouge possible (ou très probablement pas).


1
Eh bien, il peut y avoir «trop» d’affirmations en théorie, bien que le problème devienne évident très rapidement: si l’affirmation prend beaucoup plus de temps que le fond de la fonction. Certes, je ne me souviens pas d’avoir trouvé que, dans la nature, le problème inverse est répandu.
Déduplicateur

@DuDuplicator Ah oui, j'ai rencontré ce cas dans ces routines critiques de calcul vectoriel. Bien qu'il semble définitivement préférable de pécher par excès que par trop peu!

-1

Il est très raisonnable d'ajouter des contrôles à votre code. Pour assert simple (celui intégré dans les compilateurs C et C ++), mon schéma d’utilisation est qu’une assertion échouée signifie qu’un bogue dans le code doit être corrigé. J'interprète cela un peu généreusement; si je m'attends à ce qu'une demande Web renvoie un statut 200 et l'assert pour elle sans traiter d'autres cas, une assertion ayant échoué indique un bogue dans mon code, donc l' assertion est justifiée.

Donc, quand les gens disent une affirmation qui vérifie seulement ce que fait le code est superflue, ce n'est pas tout à fait correct. Cette assertion vérifie ce qu’ils pensent que le code fait, et l’essentiel de cette assertion est de vérifier que l’hypothèse de non-bug dans le code est correcte. Et l'assertion peut aussi servir de documentation. Si je suppose qu'après l'exécution d'une boucle, i == n et que le code ne l'indique pas à 100%, "assert (i == n)" sera utile.

Il vaut mieux avoir plus que simplement "affirmer" dans votre répertoire pour gérer différentes situations. Par exemple, la situation où je vérifie qu'il ne se passe pas quelque chose qui indiquerait un bogue, mais continue de travailler autour de cette condition. (Par exemple, si j'utilise un cache, je pourrais vérifier les erreurs et, si une erreur se produit de manière inattendue, il peut être sûr de corriger l'erreur en jetant le cache. Je veux quelque chose qui est presque une assertion, qui me l'indique pendant le développement. et me laisse toujours continuer.

Un autre exemple est la situation dans laquelle je ne m'attends pas à quelque chose, j'ai une solution générique, mais si cela se produit, je veux le savoir et l'examiner. Encore une fois, cela ressemble presque à une affirmation, cela devrait me le dire pendant le développement. Mais pas tout à fait une affirmation.

Trop d'assertions: Si une assertion bloque votre programme lorsqu'il est entre les mains de l'utilisateur, vous ne devez avoir aucune assertion qui se bloque à cause de faux négatifs.


-3

Ça dépend. Si les exigences du code sont clairement documentées, l'assertion doit toujours correspondre aux exigences. Dans ce cas, c'est une bonne chose. Cependant, s'il n'y a pas d'exigences ou d'exigences mal écrites, il serait alors difficile pour les nouveaux programmeurs d'éditer du code sans avoir à se référer au test unitaire à chaque fois pour déterminer quelles sont les exigences.


3
cela ne semble pas offrir quoi que ce soit de substantiel sur les points soulevés et expliqués dans les 8 réponses précédentes
gnat
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.