Je suis un grand fan de l'écriture de assert
vé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)))))
la source
Réponses:
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 .
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é.
la source
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.
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.
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.
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.
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.
Vous pouvez maintenant utiliser les trois macros
MY_ASSERT_LOW
,MY_ASSERT_MEDIUM
etMY_ASSERT_HIGH
au lieu de laassert
macro "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é-processeurMY_ASSERT_COST_LIMIT
pour sélectionner le type d'assertions à inclure dans l'exécutable. Les constantesMY_ASSERT_COST_NONE
etMY_ASSERT_COST_ALL
ne correspondent pas aux macros assert et sont destinés à être utilisés comme valeursMY_ASSERT_COST_LIMIT
afin 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
et transformer
dans
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))
surmy::assertion_failed
ou__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 fonctionmy::assertion_failed
par quelque chose comme__builtin_trap
pour 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
est compilé dans l'assemblage suivant
tandis que le code suivant
donne cette assemblée
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
,-O3
et des-march=native
drapeaux sur 4.3.3-2-ARCH x86_64 GNU / Linux. Ne figurent pas dans les extraits ci - dessus sont les déclarations detest::positive_difference_1st
ettest::positive_difference_2nd
que j'ajouté le__attribute__ ((hot))
à.my::assertion_failed
A é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é.
Au lieu d'écrire
à chaque site d'appel, mettez cette logique une fois dans la définition de
count_letters
et appelez-le sans plus tarder.
Cela présente les avantages suivants.
assert
instructions dans votre code.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.
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 simpleassert(this->good())
comme première ligne de la miseoperator*
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.
Cela donnera aussi plus de messages d'erreur utiles.
aide beaucoup plus que, disons
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
class
des 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
private
fonction membre que j'appelle de manière conventionnelleclass_invaraiants_hold_
. Supposons que vous soyez en train de ré-implémenterstd::vector
(parce que nous savons tous que ce n'est pas assez bon), cela pourrait avoir une fonction comme celle-ci.Notez quelques petites choses à ce sujet.
const
etnoexcept
, conformément à la directive que les affirmations ne doivent pas avoir des effets secondaires. Si cela a du sens, déclarez-le égalementconstexpr
.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.if
instructions avecreturn
s 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.
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::vector
fonctionne 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.
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.
C'est mieux que la solution suivante qui ne vous dira pas si vos hypothèses étaient correctes.
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…
…au lieu de.
De même, n’utilisez jamais d’assertions pour valider des entrées non fiables ou des vérifications qui
std::malloc
nereturn
vous ont pas été attribuéesnullptr
. 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'unethrow
exception. 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 pourassert
, 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.
la source
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.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.
la source
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).
la source
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:
la source
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.
la source
Je veux travailler avec vous! Quelqu'un qui écrit beaucoup
asserts
est 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 simpleassert
.Message d'échec
La seule chose à laquelle je peux penser est d'intégrer des informations sur les défaillances dans le
assert
cas où vous ne le faites pas déjà, comme ceci: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,
assert
on peut effectivement en abuser et introduire des erreurs, comme ceci:... 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:
... 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 pasoperator[]
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).
la source
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.
la source
Ç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.
la source