Faut-il vérifier chaque petite erreur dans C?

60

En tant que bon programmeur, vous devez écrire des codes robustes capables de gérer chaque résultat de son programme. Cependant, presque toutes les fonctions de la bibliothèque C renverront 0 ou -1 ou NULL en cas d'erreur.

Il est parfois évident qu'une vérification d'erreur est nécessaire, par exemple lorsque vous essayez d'ouvrir un fichier. Mais j'ignore souvent la vérification des erreurs dans des fonctions telles que printfou même mallocparce que je ne me sens pas nécessaire.

if(fprintf(stderr, "%s", errMsg) < 0){
    perror("An error occurred while displaying the previous error.");
    exit(1);
}

Est-ce une bonne pratique de simplement ignorer certaines erreurs ou existe-t-il un meilleur moyen de gérer toutes les erreurs?

Derek 會 功夫
la source
13
Dépend du niveau de robustesse requis pour le projet sur lequel vous travaillez. Les systèmes qui ont une chance de recevoir des entrées de parties non fiables (par exemple des serveurs faisant face au public), ou fonctionnant dans des environnements qui ne sont pas totalement fiables, doivent être codés très prudemment, pour éviter que le code ne devienne une bombe à retardement (ou que le lien le plus faible soit piraté ). De toute évidence, les projets de loisir et d’apprentissage n’ont pas besoin d’une telle robustesse.
Rwong
1
Certaines langues fournissent des exceptions. Si vous n'acceptez pas les exceptions, votre thread se terminera, ce qui est mieux que de le laisser continuer avec le mauvais état. Si vous choisissez de capturer des exceptions, vous pouvez couvrir de nombreuses erreurs dans de nombreuses lignes de code, y compris des fonctions et méthodes invoquées avec une seule tryinstruction. Vous n'avez donc pas à vérifier chaque appel ou opération. (Notez également que certaines langues sont plus aptes que d'autres à détecter des erreurs simples telles que la déréférence null ou l'index de tableau hors limites.)
Erik Eidt
1
Le problème est principalement d'ordre méthodologique: vous n'écrivez généralement pas de vérification d'erreur lorsque vous déterminez toujours ce que vous êtes censé mettre en œuvre et vous ne voulez pas ajouter de vérification d'erreurs maintenant, car vous souhaitez obtenir le "chemin heureux". à droite en premier. Mais vous êtes toujours censé vérifier malloc et co. Au lieu de sauter l'étape de vérification des erreurs, vous devez hiérarchiser vos activités de codage et laisser la vérification des erreurs être une étape implicite de refactoring permanent dans votre liste TODO, appliquée dès que vous êtes satisfait de votre code. Ajouter des commentaires greppable "/ * CHECK * /" peut aider.
Coredump
7
Je ne peux pas croire que personne ne soit mentionné errno! Au cas où vous ne le sauriez pas, s'il est vrai que "presque toutes les fonctions de la bibliothèque C renverront 0 ou -1 ou NULLen cas d'erreur", elles définissent également la errnovariable globale , à laquelle vous pouvez accéder en #include <errno.h>lisant simplement la valeur de errno. Ainsi, par exemple, si open(2) renvoie -1, vous souhaiterez peut-être vérifier si errno == EACCES, ce qui indiquerait une erreur d'autorisation ou ENOENTsi le fichier demandé n'existe pas.
wchargin
1
@ErikEidt C ne supporte pas try/ catch, bien que vous puissiez le simuler avec des sauts.
Mât

Réponses:

29

En général, le code devrait traiter des conditions exceptionnelles chaque fois que cela est approprié. Oui, ceci est une déclaration vague.

Dans les langages de niveau supérieur avec gestion des exceptions logicielles, cela est souvent indiqué comme "attrapez l'exception dans la méthode où vous pouvez réellement faire quelque chose à ce sujet". Si une erreur de fichier se produit, vous pouvez peut-être laisser la pile s'afficher dans le code de l'interface utilisateur qui peut en réalité indiquer à l'utilisateur "que votre fichier n'a pas pu être sauvegardé sur le disque". Le mécanisme d'exception engloutit efficacement "chaque petite erreur" et la gère implicitement à l'endroit approprié.

En C, vous n'avez pas ce luxe. Il existe plusieurs façons de gérer les erreurs, dont certaines sont des fonctionnalités de langage / bibliothèque, d'autres des pratiques de codage.

Est-ce une bonne pratique de simplement ignorer certaines erreurs ou existe-t-il un meilleur moyen de gérer toutes les erreurs?

Ignorer certaines erreurs? Peut être. Par exemple, il est raisonnable de supposer que l'écriture sur la sortie standard n'échouera pas. En cas d'échec, comment diriez-vous à l'utilisateur, de toute façon? Oui, il est judicieux d’ignorer certaines erreurs ou de coder de manière défensive pour les éviter. Par exemple, recherchez zéro avant de diviser.

Il existe des moyens de gérer toutes les erreurs, ou du moins la plupart d'entre elles:

  1. Vous pouvez utiliser des sauts, similaires à gotos, pour la gestion des erreurs . Bien que controversé parmi les professionnels du logiciel, il existe des utilisations valables, en particulier dans les codes intégrés et critiques pour la performance (par exemple, le noyau Linux).
  1. En cascade ifs:

    if (!<something>) {
      printf("oh no 1!");
      return;
    }
    if (!<something else>) {
      printf("oh no 2!");
      return;
    }
  2. Testez la première condition, par exemple en ouvrant ou en créant un fichier, puis supposez que les opérations suivantes aboutissent.

Le code robuste est bon, et il faut vérifier et gérer les erreurs. La méthode la mieux adaptée à votre code dépend de son rôle, de la gravité de la défaillance, etc., et vous seul pouvez réellement y répondre. Cependant, ces méthodes sont testées au combat et utilisées dans divers projets open source où vous pouvez jeter un coup d'œil à la façon dont le code réel vérifie les erreurs.

Communauté
la source
21
Désolé, un nit mineur à choisir ici - "l'écriture sur la sortie standard n'échouera pas. Si cela échouait, comment diriez-vous à l'utilisateur, de toute façon?" - en écrivant à l'erreur standard? Rien ne garantit qu'un défaut d'écriture sur l'un implique qu'il est impossible d'écrire sur l'autre.
Damien_The_Unbeliever
4
@ Damien_The_Unbeliever - surtout depuis stdoutpeut être redirigé vers un fichier, ou même ne pas exister selon le système. stdoutn’est pas un flux "écrire sur la console", c’est généralement cela, mais ce n’est pas nécessairement le cas.
Davor Ždralo
3
@JAB Vous envoyez un message à stdout... évident :-)
TripeHound
7
@JAB: Vous pouvez quitter avec EX_IOERR ou autre, si cela convient.
John Marshall
6
Quoi que vous fassiez, ne continuez pas à courir comme si rien ne s'était passé! Si la commande dump_database > backups/db_dump.txtne parvient pas à écrire sur la sortie standard à un moment donné, je ne voudrais pas qu'elle continue et se termine avec succès. (Non pas que les bases de données soient sauvegardées de cette façon, mais le problème est toujours valable)
Ben S
15

Cependant, presque toutes les fonctions de la bibliothèque C renverront 0 ou -1 ou NULL en cas d'erreur.

Oui, mais vous savez quelle fonction vous avez appelée, n'est-ce pas?

En fait, vous avez beaucoup d'informations que vous pourriez mettre dans un message d'erreur. Vous savez quelle fonction a été appelée, le nom de la fonction qui l'a appelée, quels paramètres ont été transmis et la valeur de retour de la fonction. C'est beaucoup d'informations pour un message d'erreur très informatif.

Vous n'êtes pas obligé de faire cela pour chaque appel de fonction. Mais la première fois que vous voyez le message d'erreur "Une erreur s'est produite lors de l'affichage de l'erreur précédente", alors que vous aviez réellement besoin d'informations utiles, ce sera la dernière fois que vous verrez ce message d'erreur car vous allez immédiatement changer le message d'erreur à quelque chose d'informatif qui vous aidera à résoudre le problème.

Robert Harvey
la source
5
Cette réponse m'a fait sourire, parce que c'est vrai, mais ne répond pas à la question.
RubberDuck
Comment ça ne répond pas à la question?
Robert Harvey
2
Une des raisons pour lesquelles je pense que C (et d’autres langues) a confondu le booléen 1 et 0 est la même raison que toute fonction décente renvoyant un code d’erreur renvoie "0" sans erreur. mais alors tu dois dire if (!myfunc()) {/*proceed*/}. 0 n'était pas une erreur, et tout ce qui n'est pas zéro est une sorte d'erreur et chaque erreur a son propre code différent de zéro. La façon dont un de mes amis l’a exprimé était «il n’ya qu’une vérité, mais beaucoup de mensonges». "0" doit être "vrai" dans le if()test et tout élément non nul doit être "faux".
robert bristow-johnson
1
non, faites-le à votre façon, il n'y a qu'un seul code d'erreur négatif possible. et beaucoup de codes no_error possibles.
robert bristow-johnson
1
@ robertbristow-johnson: lisez-le ensuite comme suit: "Il n'y a pas eu d'erreur." if(function()) { // do something }se lit comme "si la fonction s'exécute sans erreur, alors fait quelque chose." ou encore mieux, "si la fonction s'exécute avec succès, faites quelque chose". Sérieusement, ceux qui comprennent la convention ne sont pas déconcertés. Retourner false(ou 0) lorsqu'une erreur se produit ne permettrait pas l'utilisation de codes d'erreur. Zéro signifie faux en C parce que c'est comme ça que les mathématiques fonctionnent. Voir programmers.stackexchange.com/q/198284
Robert Harvey
11

TLDR; vous ne devriez presque jamais ignorer les erreurs.

Le langage C ne dispose pas d’une bonne fonctionnalité de gestion des erreurs, laissant à chaque développeur de bibliothèque l’implémentation de ses propres solutions. Des langages plus modernes ont des exceptions intégrées qui rendent ce problème particulier beaucoup plus facile à gérer.

Mais quand vous êtes coincé avec C, vous n'avez aucun de ces avantages. Malheureusement, vous devrez simplement payer le prix chaque fois que vous appelez une fonction pour laquelle il existe un risque faible d'échec. Sinon, vous subirez des conséquences bien pires, telles que l’écrasement involontaire de données en mémoire. Donc, en règle générale, vous devez toujours rechercher les erreurs.

Si vous ne vérifiez pas le retour de fprintfvotre ordinateur, il est très probable que vous laissiez un bogue qui, dans le meilleur des cas, ne ferait pas ce que l'utilisateur attend et dans le pire des cas, explose tout en vol. Il n'y a aucune excuse pour vous saper de cette façon.

Cependant, en tant que développeur C, vous devez également faciliter la maintenance du code. Il est donc parfois possible d'ignorer des erreurs par souci de clarté si (et seulement si) elles ne constituent pas une menace pour le comportement général de l'application. .

C'est le même problème que cela:

try
{
    run();
} catch (Exception) {
    // comments expected here!!!
}

Si vous voyez cela sans aucun bon commentaire dans le bloc de capture vide, c'est certainement un problème. Il n'y a aucune raison de penser qu'un appel à malloc()s'exécutera avec succès 100% du temps.

Alex
la source
Je conviens que la maintenabilité est un aspect très important et ce paragraphe a vraiment répondu à ma question. Merci pour la réponse détaillée.
Derek 會 功夫
Je pense que les gazillions de programmes qui ne se soucient jamais de vérifier leurs appels malloc et qui ne se bloquent jamais sont une bonne raison d'être paresseux lorsqu'un programme de bureau n'utilise que quelques Mo de mémoire.
Whatsisname
5
@whatsisname: Ce n'est pas parce qu'ils ne plantent pas qu'ils ne corrompent pas les données en silence. malloc()est une fonction sur laquelle vous voulez absolument vérifier les valeurs de retour!
TMN
1
@TMN: Si malloc échouait, le programme se segmenterait immédiatement et se terminerait, il ne continuerait pas à faire des choses. Tout est une question de risque et de ce qui est approprié, pas "presque jamais".
Whatsisname
2
@Alex C ne manque pas d'une bonne gestion des erreurs. Si vous voulez des exceptions, vous pouvez les utiliser. La bibliothèque standard n'utilisant pas d'exceptions est un choix délibéré (les exceptions vous obligent à utiliser une gestion de mémoire automatisée et vous ne le souhaitez souvent pas pour du code de bas niveau).
martinkunev
9

La question n'est pas réellement spécifique à la langue, mais plutôt à l'utilisateur. Pensez à la question du point de vue de l'utilisateur. L'utilisateur fait quelque chose, comme taper le nom du programme sur une ligne de commande et appuyer sur Entrée. Qu'attend l'utilisateur? Comment peuvent-ils savoir si quelque chose s'est mal passé? Peuvent-ils se permettre d'intercéder en cas d'erreur?

Dans de nombreux types de code, ces vérifications sont excessives. Cependant, dans les codes critiques de sécurité à haute fiabilité, tels que ceux des réacteurs nucléaires, la vérification des erreurs pathologiques et les chemins de récupération planifiés font partie de la nature quotidienne du travail. Prendre le temps de demander "Que se passe-t-il si X échoue? Comment puis-je revenir à un état sûr?" Dans un code moins fiable, comme celui des jeux vidéo, vous pouvez vous en tirer avec beaucoup moins de vérification d'erreur.

Une autre approche similaire consiste à déterminer dans quelle mesure vous pouvez réellement améliorer l’état en détectant l’erreur. Je ne peux pas compter le nombre de programmes C ++ qui capturent fièrement des exceptions, uniquement pour les redistribuer, car ils ne savaient pas vraiment quoi en faire ... mais ils savaient qu'ils étaient censés gérer les exceptions. De tels programmes ne tiraient aucun profit de l'effort supplémentaire. N'ajoutez que le code de vérification d'erreur qui, selon vous, pourrait mieux gérer la situation que la simple vérification du code d'erreur. Cela étant dit, C ++ a des règles spécifiques pour gérer les exceptions qui se produisent lors de la gestion des exceptions afin de les intercepter de manière plus significative (et par là, j'entends appeler terminate () pour m'assurer que le bûcher funéraire que vous avez construit vous-même s'allume. dans sa propre gloire)

Comment pathologique pouvez-vous être? J'ai travaillé sur un programme qui définit un "SafetyBool" dont les valeurs vraies et fausses ont été choisies avec soin pour avoir une distribution uniforme de 1 et de 0, et qui ont été choisies de manière à ce que l'une des nombreuses défaillances matérielles (basculement d'un bit, bus de données) traces brisées, etc.) n’a pas incité un booléen à être mal interprété. Inutile de dire que je ne prétends pas qu'il s'agisse d'une pratique de programmation à usage général pouvant être utilisée dans un programme ancien.

Cort Ammon
la source
6
  • Différentes exigences de sécurité exigent différents niveaux de correction. Dans les logiciels de contrôle de l'aviation ou de l'automobile, toutes les valeurs de retour seront vérifiées, cf. MISRA.FUNC.UNUSEDRET . Dans une rapide démonstration de concept qui ne quitte jamais votre machine, probablement pas.
  • Le codage coûte du temps. Trop de vérifications non pertinentes dans des logiciels non critiques pour la sécurité sont des efforts qu'il vaut mieux dépenser ailleurs. Mais où est le compromis entre qualité et coût? Cela dépend des outils de débogage et de la complexité du logiciel.
  • La gestion des erreurs peut masquer le flux de contrôle et introduire de nouvelles erreurs. J'aime assez Richard "réseau" wrapper fonctions de Stevens qui au moins signaler des erreurs.
  • La gestion des erreurs peut, rarement, être un problème de performances. Mais la plupart des appels de bibliothèque C prendront tellement de temps que le coût de la vérification d'une valeur de retour est extrêmement faible.
Peter - Réintégrer Monica
la source
3

Un peu abstrait résume la question. Et ce n'est pas nécessairement pour le langage C.

Pour les programmes plus importants, vous auriez une couche d'abstraction; peut-être une partie du moteur, une bibliothèque ou dans un cadre. Cette couche ne se soucient de temps , vous obtenez des données valides ou la sortie serait une valeur par défaut: 0, -1, nulletc.

Ensuite, il y a une couche qui serait votre interface avec la couche abstraite, qui ferait beaucoup de traitement des erreurs et peut-être d'autres choses comme des injections de dépendances, l'écoute d'événements, etc.

Et plus tard, vous aurez votre couche d’implémentation concrète où vous définissez les règles et gérez la sortie.

Selon moi, il est parfois préférable d'exclure complètement le traitement des erreurs d'une partie du code, car cette partie ne fait tout simplement pas ce travail. Et puis avoir un processeur qui évaluerait la sortie et indiquerait une erreur.

Ceci est principalement fait pour séparer les responsabilités qui mènent à la lisibilité du code et à une meilleure évolutivité.

Magie créative
la source
0

En général, à moins que vous n'ayez une bonne raison de ne pas vérifier cette condition d'erreur, vous devriez le faire. La seule bonne raison à laquelle je puisse penser pour ne pas rechercher une condition d'erreur est lorsqu'il est impossible de faire quelque chose de significatif en cas d'échec. C'est un obstacle très difficile à rencontrer, car il existe toujours une option viable: sortir avec élégance.

Néologisme intelligent
la source
Je ne suis généralement pas d'accord avec vous, car les erreurs sont des situations que vous n'avez pas prises en compte. Il est difficile de savoir comment l'erreur peut se manifester si vous ne savez pas dans quelle condition l'erreur s'est produite. Et donc le traitement des erreurs avant le traitement des données.
Creative Magic
De même, dis-je, si quelque chose revient ou génère une erreur, même si vous ne savez pas comment la corriger et la poursuivre, vous pouvez toujours échouer normalement, consigner l'erreur, dire à l'utilisateur que cela ne fonctionne pas comme prévu, etc. Le fait est que l’erreur ne devrait pas passer inaperçue, ne pas être enregistrée, "échouer de manière silencieuse" ou faire planter l’application. C'est ce que "échouer gracieusement" ou "sortir gracieusement" signifie.
Néologisme intelligent