L'utilisation d'assert () en C ++ est-elle une mauvaise pratique?

92

J'ai tendance à ajouter beaucoup d'assertions à mon code C ++ pour faciliter le débogage sans affecter les performances des versions de version. Maintenant, assertest une macro C pure conçue sans mécanismes C ++ à l'esprit.

C ++ d'autre part définit std::logic_error, qui est censé être levé dans les cas où il y a une erreur dans la logique du programme (d'où le nom). Lancer une instance pourrait bien être l'alternative parfaite, plus C ++ assert.

Le problème est que , assertet à la abortfois mettre fin au programme immédiatement sans appeler Destructeurs, sauter donc le nettoyage, alors lancer une exception ajoute manuellement les coûts d'exécution inutiles. Une façon de contourner cela serait de créer une propre macro d'assertion SAFE_ASSERT, qui fonctionne exactement comme l'homologue C, mais lève une exception en cas d'échec.

Je peux penser à trois opinions sur ce problème:

  • Tenez-vous en à l'affirmation de C. Étant donné que le programme se termine immédiatement, peu importe que les modifications soient correctement déroulées. De plus, l'utilisation de #defines en C ++ est tout aussi mauvaise.
  • Lancez une exception et attrapez-la dans main () . Autoriser le code à ignorer les destructeurs dans n'importe quel état du programme est une mauvaise pratique et doit être évité à tout prix, tout comme les appels à terminate (). Si des exceptions sont lancées, elles doivent être interceptées.
  • Lancez une exception et laissez-le terminer le programme. Une exception mettant fin à un programme est acceptable, et à cause de NDEBUGcela, cela ne se produira jamais dans une version de version. La capture est inutile et expose les détails d'implémentation du code interne à main().

Y a-t-il une réponse définitive à ce problème? Une référence professionnelle?

Modifié: sauter les destructeurs n'est, bien sûr, pas un comportement indéfini.

Fabian Knorr
la source
22
Non, vraiment, logic_errorc'est l'erreur logique. Une erreur dans la logique du programme est appelée un bogue. Vous ne résolvez pas les bogues en lançant des exceptions.
R. Martinho Fernandes
4
Assertions, exceptions, codes d'erreur. Chacun a un cas d'utilisation complètement distinct, et vous ne devriez pas en utiliser un là où un autre est nécessaire.
Kerrek SB
5
Assurez-vous de l'utiliser static_assertlà où c'est approprié si vous en avez à disposition.
Flexo
4
@trion Je ne vois pas en quoi cela aide. Souhaitez-vous jeter std::bug?
R. Martinho Fernandes
3
@trion: Ne fais pas ça. Les exceptions ne concernent pas le débogage. Quelqu'un pourrait attraper l'exception. Il n'y a pas besoin de s'inquiéter d'UB lors de l'appel std::abort(); il déclenchera simplement un signal qui entraînera la fin du processus.
Kerrek SB

Réponses:

73

Les assertions sont tout à fait appropriées dans le code C ++. Les exceptions et autres mécanismes de gestion des erreurs ne sont pas vraiment destinés à la même chose que les assertions.

La gestion des erreurs concerne les cas où il est possible de récupérer ou de signaler correctement une erreur à l'utilisateur. Par exemple, s'il y a une erreur lors de la tentative de lecture d'un fichier d'entrée, vous voudrez peut-être faire quelque chose à ce sujet. Des erreurs peuvent résulter de bogues, mais elles peuvent également être simplement la sortie appropriée pour une entrée donnée.

Les affirmations sont destinées à des choses comme vérifier que les exigences d'une API sont remplies lorsque l'API ne serait normalement pas vérifiée, ou pour vérifier des choses que le développeur pense être garanties par la construction. Par exemple, si un algorithme nécessite une entrée triée, vous ne le vérifieriez normalement pas, mais vous pourriez avoir une assertion pour le vérifier afin que les builds de débogage signalent ce type de bogue. Une assertion doit toujours indiquer un programme fonctionnant de manière incorrecte.


Si vous écrivez un programme dans lequel un arrêt impur pourrait causer un problème, vous voudrez peut-être éviter les assertions. Un comportement non défini strictement en termes de langage C ++ ne constitue pas un tel problème ici, car frapper une assertion est probablement déjà le résultat d'un comportement non défini, ou de la violation d'une autre exigence qui pourrait empêcher un nettoyage de fonctionner correctement.

De plus, si vous implémentez des assertions en termes d'exception, elle pourrait potentiellement être interceptée et «gérée» même si cela contredit le but même de l'assertion.

bames53
la source
1
Je ne suis pas tout à fait sûr que cela ait été indiqué spécifiquement dans la réponse, je vais donc le dire ici: vous ne devriez pas utiliser une assertion pour tout ce qui implique une entrée utilisateur qui ne peut pas être déterminée au moment de l'écriture du code. Si un utilisateur passe à la 3place de 1votre code, il ne doit en général pas déclencher d'assertion. Les affirmations ne sont qu'une erreur de programmeur, pas une erreur d'utilisateur de la bibliothèque ou d'application.
SS Anne
101
  • Les assertions sont destinées au débogage . L'utilisateur de votre code livré ne devrait jamais les voir. Si une assertion est atteinte, votre code doit être corrigé.

    CWE-617: Assertion accessible

Le produit contient une assert () ou une instruction similaire qui peut être déclenchée par un attaquant, ce qui conduit à une sortie d'application ou à un autre comportement plus sévère que nécessaire.

Bien que l'assertion soit bonne pour détecter les erreurs de logique et réduire les chances d'atteindre des conditions de vulnérabilité plus graves, elle peut néanmoins conduire à un déni de service.

Par exemple, si un serveur gère plusieurs connexions simultanées et qu'un assert () se produit dans une seule connexion qui entraîne la suppression de toutes les autres connexions, il s'agit d'une assertion atteignable qui conduit à un déni de service.

  • Les exceptions concernent des circonstances exceptionnelles . S'il en rencontre un, l'utilisateur ne pourra pas faire ce qu'il veut, mais pourra peut-être reprendre ailleurs.

  • La gestion des erreurs concerne le déroulement normal du programme. Par exemple, si vous demandez à l'utilisateur un nombre et obtenez quelque chose d'imparsable, c'est normal , car l'entrée de l'utilisateur n'est pas sous votre contrôle et vous devez toujours gérer toutes les situations possibles naturellement. (Par exemple, faites une boucle jusqu'à ce que vous ayez une entrée valide, en disant "Désolé, réessayez" entre les deux.)

Kerrek SB
la source
1
est venu chercher cette re affirmer; toute forme d'affirmation passant par le code de production indique une conception et une assurance qualité médiocres. Le point où une assertion est appelée est l'endroit où la gestion gracieuse d'une condition d'erreur devrait être. (Je n'utilise jamais d' assert). En ce qui concerne les exceptions, le seul cas d'utilisation que je connaisse est celui où le ctor peut échouer, tous les autres concernent la gestion normale des erreurs.
slashmais
5
@slashmais: Le sentiment est louable, mais à moins que vous ne livriez un code parfait et sans bogue, je trouve une assertion (même celle qui bloque l'utilisateur) préférable à un comportement non défini. Les bogues se produisent dans des systèmes complexes, et avec une assertion, vous avez un moyen de le voir et de le diagnostiquer là où il se produit.
Kerrek SB
@KerrekSB Je préfère utiliser une exception plutôt qu'une assertion. Au moins, le code a une chance de supprimer la branche défaillante et de faire quelque chose d'utile. À tout le moins, si vous utilisez RAII, tous vos tampons pour ouvrir les fichiers seront vidés correctement.
daemonspring
14

Les assertions peuvent être utilisées pour vérifier les invariants internes d'implémentation, comme l'état interne avant ou après l'exécution d'une méthode, etc. Dans ce cas, le mieux que vous puissiez faire est de rompre le plus tôt possible sans faire exception à l'utilisateur. Ce qui est vraiment bien avec les assertions (au moins sous Linux), c'est que le vidage de mémoire est généré à la suite de l'arrêt du processus et que vous pouvez ainsi facilement étudier la trace de la pile et les variables. Ceci est beaucoup plus utile pour comprendre l'échec logique que le message d'exception.

Nogard
la source
J'ai une approche similaire. J'utilise des assertions pour la logique qui devrait être probablement correcte localement (par exemple, les invariants de boucle). Les exceptions concernent les cas où une erreur logique a été imposée au code par une situation non locale (externe).
spraff le
Si une assertion échoue, cela signifie que la logique d'une partie du programme est rompue. Une assertion ratée n'implique pas nécessairement que rien ne peut être accompli. Un plugin cassé ne devrait probablement pas interrompre un traitement de texte entier.
daemonspring
13

Ne pas exécuter de destructeurs à cause de tout abort () n'est pas un comportement indéfini!

Si c'était le cas, alors ce serait un comportement indéfini à appeler std::terminate()aussi, et alors quel serait l'intérêt de le fournir?

assert() est tout aussi utile en C ++ qu'en C. Les assertions ne servent pas à gérer les erreurs, mais à abandonner immédiatement le programme.

Jonathan Wakely
la source
1
Je dirais que abort()c'est pour abandonner le programme immédiatement. Vous avez raison de dire que les assertions ne sont pas destinées à la gestion des erreurs, mais assert essaie de gérer l'erreur en abandonnant. Ne devriez-vous pas plutôt lever une exception et laisser l'appelant gérer l'erreur s'il le peut? Après tout, l'appelant est mieux placé pour déterminer si l'échec d'une fonction ne vaut pas la peine de faire autre chose. Peut-être que l'appelant essaie de faire trois choses sans rapport et pourrait encore terminer les deux autres tâches et simplement supprimer celle-ci.
daemonspring
Et assertest défini pour appeler abort(lorsque la condition est fausse). Quant à lancer des exceptions, non, ce n'est pas toujours approprié. Certaines choses ne peuvent pas être gérées par l'appelant. L'appelant ne peut pas déterminer si un bogue logique dans une fonction de bibliothèque tierce est récupérable ou si des données corrompues peuvent être corrigées.
Jonathan Wakely le
6

À mon humble avis, les affirmations servent à vérifier les conditions qui, si elles sont violées, rendent tout le reste absurde. Et par conséquent, vous ne pouvez pas récupérer d'eux ou plutôt, la récupération n'est pas pertinente.

Je les regrouperais en 2 catégories:

  • Les péchés du développeur (par exemple, une fonction de probabilité qui renvoie des valeurs négatives):

float probabilit () {return -1.0; }

assert (probabilité ()> = 0,0)

  • La machine est cassée (par exemple, la machine qui exécute votre programme est très mauvaise):

int x = 1;

assert (x> 0);

Ce sont deux exemples triviaux mais pas trop éloignés de la réalité. Par exemple, pensez aux algorithmes naïfs qui renvoient des index négatifs à utiliser avec des vecteurs. Ou des programmes intégrés dans du matériel personnalisé. Ou plutôt parce que sh * t arrive .

Et s'il y a de telles erreurs de développement, vous ne devriez pas être sûr de tout mécanisme de récupération ou de gestion des erreurs implémenté. Il en va de même pour les erreurs matérielles.

FranMowinckel
la source
1
assert (probabilité ()> = 0.0)
Elliott