web-dev-qa-db-fra.com

Devrait-il y avoir des assertions dans les versions

Le comportement par défaut de assert en C++ est de ne rien faire dans les versions. Je suppose que cela est fait pour des raisons de performances et peut-être pour empêcher les utilisateurs de voir des messages d'erreur désagréables.

Cependant, je dirais que les situations où un assert se serait déclenché mais aurait été désactivé sont encore plus gênantes car l'application se bloquera probablement de manière encore pire sur toute la ligne car un invariant a été cassé.

De plus, l'argument de performance pour moi ne compte que lorsqu'il s'agit d'un problème mesurable. La plupart des assert de mon code ne sont pas beaucoup plus complexes que

assert(ptr != nullptr);

ce qui aura un faible impact sur la plupart du code.

Cela m'amène à la question: les assertions (c'est-à-dire le concept, pas l'implémentation spécifique) doivent-elles être actives dans les versions de versions? Pourquoi pas)?

Veuillez noter que cette question ne concerne pas la façon d'activer les assertions dans les versions (comme #undef _NDEBUG ou en utilisant une implémentation d'assertion auto-définie). De plus, il ne s'agit pas d'activer des assertions dans du code de bibliothèque tiers/standard mais dans du code que je contrôle.

22
Nobody

Le classique assert est un outil de l'ancienne bibliothèque C standard, pas de C++. Il est toujours disponible en C++, au moins pour des raisons de compatibilité descendante.

Je n'ai pas de chronologie précise des bibliothèques standard C à portée de main, mais je suis presque sûr que assert était disponible peu de temps après la mise en service de K&R C (vers 1978). En C classique, pour écrire des programmes robustes, l'ajout de tests de pointeur NULL et la vérification des limites du tableau doivent être effectués beaucoup plus fréquemment qu'en C++. Le brut des tests de pointeur NULL peut être évité en utilisant des références et/ou des pointeurs intelligents au lieu de pointeurs, et en utilisant std::vector, la vérification des limites du tableau est souvent inutile. De plus, la performance atteinte en 1980 était nettement plus importante qu'aujourd'hui. Je pense donc que c'est très probablement la raison pour laquelle "assert" a été conçu pour être actif uniquement dans les versions de débogage par défaut.

De plus, pour une gestion réelle des erreurs dans le code de production, une fonction qui teste simplement une condition ou un invariant, et plante le programme si la condition n'est pas remplie, n'est dans la plupart des cas pas assez flexible. Pour le débogage, c'est probablement correct, car celui qui exécute le programme et observe l'erreur a généralement un débogueur à portée de main pour analyser ce qui se passe. Pour le code de production, cependant, une solution sensée doit être une fonction ou un mécanisme qui

  • teste une condition (et arrête l'exécution à la portée où la condition échoue)

  • fournit un message d'erreur clair dans le cas où la condition ne tient pas

  • permet à la portée externe de prendre le message d'erreur et de le transmettre à un canal de communication spécifique. Ce canal peut être quelque chose comme stderr, un fichier journal standard, une boîte de message dans un programme GUI, un rappel général de gestion des erreurs, un canal d'erreur activé par le réseau ou tout ce qui convient le mieux à un logiciel particulier.

  • permet à la portée externe au cas par cas de décider si le programme doit se terminer correctement ou s'il doit continuer.

(Bien sûr, il existe également des situations où la fermeture immédiate du programme en cas de condition non remplie est la seule option raisonnable, mais dans de tels cas, cela devrait également se produire dans une version de génération, pas seulement dans une version de débogage).

Étant donné que le classique assert ne fournit pas ces fonctionnalités, il ne convient pas à une version de version, en supposant que la version de version est ce que l'on déploie en production.

Vous pouvez maintenant vous demander pourquoi il n'y a pas une telle fonction ou mécanisme dans la bibliothèque standard C qui offre ce type de flexibilité. En fait, en C++, il existe un mécanisme standard qui a toutes ces fonctionnalités (et plus), et vous le savez: il s'appelle exceptions.

En C, cependant, il est difficile de mettre en œuvre un bon mécanisme standard à usage général pour la gestion des erreurs avec toutes les fonctionnalités mentionnées en raison de l'absence d'exceptions dans le cadre du langage de programmation. Ainsi, la plupart des programmes C ont leurs propres mécanismes de gestion des erreurs avec des codes de retour, ou "goto", ou "sauts longs", ou un mélange de cela. Ce sont souvent des solutions pragmatiques qui s’adaptent au type particulier de programme, mais qui ne sont pas "suffisamment polyvalentes" pour s’intégrer à la bibliothèque standard C.

21
Doc Brown

Si vous souhaitez que les assertions soient activées dans une version, vous avez demandé à asserts de faire le mauvais travail.

Le point des assertions est qu'elles ne sont pas activées dans une version. Cela permet de tester des invariants pendant le développement avec du code qui autrement devrait être du code d'échafaudage. Code qui doit être supprimé avant la publication.

Si vous pensez que quelque chose devrait être testé même pendant la publication, écrivez du code qui le teste. La construction If throw Fonctionne très bien. Si vous souhaitez dire quelque chose de différent des autres lancers, utilisez simplement une exception descriptive qui dit ce que vous souhaitez dire.

Ce n'est pas que vous ne pouvez pas changer la façon dont vous utilisez les assertions. C'est que cela ne vous apporte rien d'utile, va à l'encontre des attentes et ne vous laisse aucun moyen propre de faire ce que les affirmations étaient censées faire. Ajoutez des tests inactifs dans une version.

Je ne parle pas d'une implémentation spécifique de l'assert mais du concept d'une assertion. Je ne veux pas abuser d'affirmer ou de confondre les lecteurs. Je voulais d'abord demander pourquoi il en est ainsi. Pourquoi n'y a-t-il pas de release_assert supplémentaire? N'est-ce pas nécessaire? Quelle est la raison d'être de l'assertion de la désactivation lors de la libération? - Personne

Pourquoi pas relase_assert? Franchement parce que les assertions ne sont pas assez bonnes pour la production. Oui, il y a un besoin, mais rien ne répond bien à ce besoin. Oh, bien sûr, vous pouvez concevoir le vôtre. Mécaniquement, votre fonction throwIf a juste besoin d'un booléen et d'une exception à lancer. Et cela peut répondre à vos besoins. Mais vous limitez vraiment le design. C'est pourquoi cela ne me surprend pas qu'il n'y ait pas de système de levée d'exception assert comme dans votre bibliothèque de langues. Ce n'est certainement pas que vous ne pouvez pas le faire. D'autres l'ont . Mais faire face au cas où les choses tournent mal représente 80% du travail pour la plupart des programmes. Et jusqu'à présent, personne ne nous a montré une bonne solution universelle. Traiter efficacement ces cas peut devenir compliqué . Si nous avions eu un système de release_assert en conserve qui ne répondait pas à nos besoins, je pense qu'il aurait fait plus de mal que de bien. Vous demandez une bonne abstraction qui signifierait que vous n'auriez pas à penser à ce problème. J'en veux un aussi mais il ne semble pas que nous y soyons encore.

Pourquoi les assertions sont-elles désactivées dans la version? Les assertions ont été créées au plus fort de l'ère du code d'échafaudage. Code que nous avons dû supprimer car nous savions que nous ne le voulions pas en production, mais nous savions que nous voulions l'exécuter en développement pour nous aider à trouver des bogues. Les assertions étaient une alternative plus propre au modèle if (DEBUG) qui nous permettait de laisser le code mais de le désactiver. C'était avant que les tests unitaires ne décollent comme principal moyen de séparer le code de test du code de production. Les assertions sont encore utilisées aujourd'hui, même par des testeurs unitaires experts, à la fois pour clarifier les attentes et pour couvrir les cas où ils font encore mieux que les tests unitaires.

Pourquoi ne pas simplement laisser le code de débogage en production? Parce que le code de production ne doit pas embarrasser l'entreprise, ne pas formater le disque dur, ne pas corrompre la base de données et ne pas envoyer de courriels menaçants au président. En bref, c'est agréable de pouvoir écrire du code de débogage dans un endroit sûr où vous n'avez pas à vous en soucier.

16
candied_orange

Les assertions sont un outil de débogage, pas une technique de programmation défensive. Si vous souhaitez effectuer la validation en toutes circonstances, effectuez la validation en écrivant un conditionnel - ou créez votre propre macro pour réduire le passe-partout.

4
amon

assert est une forme de documentation, comme les commentaires. Comme les commentaires, vous ne les enverriez pas normalement aux clients - ils n'appartiennent pas au code de version.

Mais le problème avec les commentaires est qu'ils peuvent devenir obsolètes, et pourtant ils sont conservés. C'est pourquoi les assertions sont bonnes - elles sont vérifiées en mode débogage. Lorsque l'assertion devient obsolète, vous la découvrez rapidement et saurez toujours comment corriger l'assertion. Ce commentaire devenu obsolète il y a 3 ans? Quiconque pense.

4
MSalters

Si vous ne voulez pas qu'une "assertion" soit désactivée, n'hésitez pas à écrire une fonction simple qui a un effet similaire:

void fail_if(bool b) {if(!b) std::abort();}

Autrement dit, assert est destiné aux tests où vous faites souhaitez qu'ils disparaissent dans le produit expédié. Si vous voulez que ce test fasse partie du comportement défini du programme, assert n'est pas le bon outil.

3
Nicol Bolas

Il est inutile de discuter de ce que l’assertion devrait faire, elle fait ce qu’elle fait. Si vous voulez quelque chose de différent, écrivez votre propre fonction. Par exemple, j'ai Assert qui s'arrête dans le débogueur ou ne fait rien, j'ai AssertFatal qui plantera l'application, j'ai des fonctions booléennes Assertion et AssertionFailed qui assert et retournent le résultat afin que je puisse à la fois affirmer et gérer la situation.

Pour tout problème inattendu, vous devez décider quel est le meilleur moyen pour le développeur et l'utilisateur de le gérer.

3
gnasher729

Comme d'autres l'ont souligné, assert est en quelque sorte votre dernier bastion de défense contre les erreurs de programmation qui ne devraient jamais se produire. Ce sont des contrôles de santé mentale qui, espérons-le, ne devraient pas échouer à gauche et à droite au moment de l'expédition.

Il est également conçu pour être omis des versions de versions stables, pour toutes les raisons que les développeurs pourraient trouver utiles: esthétique, performances, tout ce qu'ils veulent. Cela fait partie de ce qui sépare une version de débogage d'une version de version, et par définition, une version de version est dépourvue de telles affirmations. Il y a donc une Subversion de la conception si vous voulez libérer l'analogue "build build avec assertions en place" qui serait une tentative de build release avec une définition de préprocesseur _DEBUG et aucun NDEBUG défini; ce n'est plus vraiment une version.

La conception s'étend même dans la bibliothèque standard. Comme exemple très basique parmi de nombreuses autres, de nombreuses implémentations de std::vector::operator[]assert feront un contrôle de cohérence pour s'assurer que vous ne vérifiez pas le vecteur hors limites. Et la bibliothèque standard commencera à fonctionner beaucoup, bien pire si vous activez de telles vérifications dans une version. Un benchmark de vector utilisant operator[] Et un ctor de remplissage avec de telles assertions incluses par rapport à un ancien tableau dynamique simple montrera souvent que le tableau dynamique est considérablement plus rapide jusqu'à ce que vous désactiviez de telles vérifications, donc elles ont souvent un impact performances de loin, loin de banales. Une vérification de pointeur nul ici et une vérification hors limites peuvent en fait devenir une dépense énorme si ces vérifications sont appliquées des millions de fois sur chaque trame dans les boucles critiques précédant le code aussi simple que de déréférencer un pointeur intelligent ou d'accéder à un tableau.

Vous souhaitez donc probablement un outil différent pour le travail et un outil qui n'est pas conçu pour être omis des versions de version si vous souhaitez des versions de version qui effectuent de tels contrôles d'intégrité dans des domaines clés. Le plus utile que je trouve personnellement est la journalisation. Dans ce cas, lorsqu'un utilisateur signale un bogue, les choses deviennent beaucoup plus faciles s'il joignent un journal et la dernière ligne du journal me donne une grande idée de l'endroit où le bogue s'est produit et de ce qu'il pourrait être. Ensuite, en reproduisant leurs étapes dans une version de débogage, je pourrais également obtenir un échec d'assertion, et cet échec d'assertion me donne en outre d'énormes indices pour rationaliser mon temps. Pourtant, étant donné que la journalisation est relativement coûteuse, je ne l'utilise pas pour appliquer des contrôles d'intégrité extrêmement bas comme pour s'assurer qu'un tableau n'est pas accessible hors limites dans une structure de données générique. Je l'utilise dans des contextes plus avancés avec plus d'informations spécifiques au domaine de l'application.

Pourtant, enfin, et quelque peu en accord avec vous, je pouvais voir un cas raisonnable où vous pourriez réellement vouloir remettre aux testeurs quelque chose ressemblant à une version de débogage pendant les tests alpha, par exemple, avec un petit groupe de testeurs alpha qui, par exemple, ont signé un NDA . Là, cela pourrait rationaliser les tests alpha si vous remettez à vos testeurs autre chose qu'une version complète avec des informations de débogage attachées ainsi que des fonctionnalités de débogage/développement comme des tests qu'ils peuvent exécuter et une sortie plus verbeuse pendant qu'ils exécutent le logiciel. J'ai au moins vu quelques grandes sociétés de jeu faire des choses comme ça pour alpha. Mais c'est pour quelque chose comme alpha ou des tests internes où vous essayez vraiment de donner aux testeurs autre chose qu'une version. Si vous essayez de livrer une version, alors par définition, elle ne devrait pas avoir défini _DEBUG, Sinon cela confond vraiment la différence entre une version "debug" et une version "release".

Pourquoi ce code doit-il être supprimé avant sa sortie? Les vérifications ne sont pas vraiment une perte de performance et si elles échouent, il y a certainement un problème sur lequel je préférerais un message d'erreur plus direct.

Comme indiqué ci-dessus, les vérifications ne sont pas nécessairement triviales du point de vue des performances. Beaucoup sont probablement triviaux, mais encore une fois, même la bibliothèque standard les utilise et cela pourrait affecter les performances de manière inacceptable pour de nombreuses personnes dans de nombreux cas si, par exemple, la traversée d'accès aléatoire de std::vector Prenait 4 fois plus de temps dans ce qui est supposé être une version optimisée en raison de sa vérification des limites qui n'est jamais censée échouer.

Dans une ancienne équipe, nous avons dû faire en sorte que notre bibliothèque de matrices et de vecteurs exclue certaines assertions dans certains chemins critiques juste pour accélérer les builds de débogage, car ces assertions ralentissaient les opérations mathématiques de plus d'un ordre de grandeur au point où elles se trouvaient. commencer à nous obliger à attendre 15 minutes avant que nous puissions même retracer dans le code d'intérêt. Mes collègues voulaient simplement supprimer purement et simplement le asserts parce qu'ils trouvaient que cela faisait une énorme différence. Au lieu de cela, nous nous sommes contentés de faire en sorte que les chemins de débogage critiques les évitent. Lorsque nous avons fait en sorte que ces chemins critiques utilisent directement les données vectorielles/matricielles sans passer par la vérification des limites, le temps nécessaire pour effectuer l'opération complète (qui comprenait plus que des calculs vectoriels/matriciels) a été réduit de quelques minutes à quelques secondes. C'est donc un cas extrême, mais les affirmations ne sont certainement pas toujours négligeables du point de vue des performances, pas même de près.

Mais c'est aussi juste la façon dont asserts est conçu. S'ils n'avaient pas un impact énorme sur les performances à tous les niveaux, je pourrais le préférer s'ils étaient conçus comme plus qu'une fonctionnalité de construction de débogage ou si nous pouvions utiliser vector::at Qui inclut la vérification des limites même dans les versions et jette sur l'accès hors limites, par exemple (mais avec un énorme succès de performance). Mais actuellement, je trouve leur conception beaucoup plus utile, compte tenu de leur énorme impact sur les performances dans mes cas, en tant que fonctionnalité de débogage uniquement qui est omise lorsque NDEBUG est défini. Pour les cas avec lesquels j'ai travaillé au moins, cela fait une énorme différence pour une version de construction d'exclure les contrôles d'intégrité qui ne devraient jamais échouer en premier lieu.

vector::at Contre vector::operator[]

Je pense que la distinction de ces deux méthodes est au cœur de cela ainsi que l'alternative: les exceptions. Les implémentations de vector::operator[] Sont généralement assert pour vous assurer que l'accès hors limites déclenchera une erreur facilement reproductible lorsque vous tenterez d'accéder à un vecteur hors limites. Mais les implémenteurs de bibliothèque le font avec l'hypothèse que cela ne coûtera pas un sou dans une version optimisée.

Pendant ce temps, vector::at Est fourni, qui effectue toujours la vérification et les lancements hors limites, même dans les versions, mais il a une pénalité de performance au point où je vois souvent beaucoup plus de code en utilisant vector::operator[] Que vector::at. Une grande partie de la conception de C++ fait écho à l'idée de "payer pour ce que vous utilisez/avez besoin", et beaucoup de gens préfèrent souvent operator[], Ce qui ne dérange même pas avec la vérification des limites dans les versions, basées sur sur la notion qu'ils n'ont pas besoin que les limites vérifient leurs versions optimisées de version. Soudain, si les assertions étaient activées dans les versions, les performances de ces deux seraient identiques et l'utilisation du vecteur finirait toujours par être plus lente qu'un tableau dynamique. Ainsi, une grande partie de la conception et des avantages des assertions est basée sur l'idée qu'elles deviennent gratuites dans une version.

release_assert

C'est intéressant après avoir découvert ces intentions. Naturellement, les cas d'utilisation de chacun seraient différents, mais je pense que je trouverais une certaine utilité pour un release_assert Qui effectue la vérification et fera planter le logiciel affichant un numéro de ligne et un message d'erreur même dans les versions.

Ce serait pour certains cas obscurs dans mon cas où je ne veux pas que le logiciel récupère gracieusement comme il le ferait si une exception était levée. Je voudrais qu'il se bloque même dans la version dans ces cas afin que l'utilisateur puisse recevoir un numéro de ligne pour signaler quand le logiciel a rencontré quelque chose qui ne devrait jamais se produire, toujours dans le domaine des vérifications d'intégrité pour les erreurs de programmation, pas les erreurs d'entrée externes comme exceptions, mais assez bon marché pour être fait sans se soucier de son coût de sortie.

Il y a en fait des cas où je trouverais un crash dur avec un numéro de ligne et un message d'erreur préférable pour récupérer gracieusement d'une exception levée qui pourrait être assez bon marché à conserver dans une version. Et il y a des cas où il est impossible de récupérer à partir d'une exception, comme une erreur rencontrée en essayant de récupérer à partir d'une exception existante. Là, je trouverais un ajustement parfait pour une release_assert(!"This should never, ever happen! The software failed to fail!"); et naturellement ce serait très bon marché puisque la vérification serait effectuée à l'intérieur d'un chemin exceptionnel en premier lieu et ne coûterait rien dans les chemins d'exécution normaux.

2
user204677

Je code en Ruby, pas en C/C++, et donc je ne parlerai pas de différence entre les assertions et les exceptions mais je voudrais en parler comme chose qui arrête le runtime. Je suis en désaccord avec la plupart des réponses ci-dessus, car l'arrêt du programme avec l'impression d'une trace arrière fonctionne très bien pour moi comme technique de programmation défensive.
S'il existe un moyen d'appeler la routine (absolument peu importe comment elle est écrite syntaxiquement et si le mot "assert" est utilisé ou existe dans le langage de programmation ou dsl) à appeler, cela signifie qu'un travail doit être fait et le produit passe immédiatement de la version "prêt à l'emploi" à "nécessite un correctif" - maintenant, soit réécrivez-le dans la gestion des exceptions réelles, soit corrigez un bogue qui faisait apparaître des données erronées.

Je veux dire que Assert n'est pas une chose avec laquelle vous devriez vivre et appeler fréquemment - c'est un signal d'arrêt qui indique que vous devez faire quelque chose pour que cela ne se reproduise plus jamais. Et dire que "build build ne devrait pas avoir d'assert", c'est comme dire "build build ne devrait pas avoir de bugs" - mec, c'est presque impossible, faites-le.
Ou pensez à eux comme à "l'échec des tests effectués et exécutés par l'utilisateur final". Vous ne pouvez pas prédire tout ce que l'utilisateur va faire avec votre programme, mais si un problème trop grave se passe mal, il devrait s'arrêter - c'est similaire à la façon dont vous construisez des pipelines de construction - vous arrêtez le processus et ne publiez pas, pensez-vous ? L'assertion force l'utilisateur à s'arrêter, à signaler et à attendre votre aide.

2
Nakilon