Est-il raisonnable d’annuler la garde de chaque pointeur déréférencé?

65

Lors d'un nouvel emploi, les critiques de code correspondant à ce code me sont signalées:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender) { }

void PowerManager::SignalShutdown()
{
    msgSender_->sendMsg("shutdown()");
}

On me dit que la dernière méthode devrait se lire:

void PowerManager::SignalShutdown()
{
    if (msgSender_) {
        msgSender_->sendMsg("shutdown()");
    }
}

c'est-à-dire que je dois mettre un NULLgarde autour de la msgSender_variable, même s'il s'agit d'un membre de données privé. Il m'est difficile de m'empêcher d'utiliser des jurons pour décrire ce que je ressens à propos de cette «sagesse». Lorsque je demande une explication, je reçois toute une série d'histoires d'horreur sur la façon dont un programmeur junior, une année sur l'autre, s'est confondu sur la façon dont une classe était censée fonctionner et a accidentellement supprimé un membre qu'il ne devrait pas avoir (et l'a mis NULLensuite à , apparemment), et les choses ont explosé dans le champ droit après une sortie du produit, et nous avons « appris à la dure, faites- nous confiance » qu'il est préférable de simplement NULLvérifier tout .

Pour moi, cela ressemble à une programmation culte de la cargaison , tout simplement. Quelques collègues bien intentionnés essaient sincèrement de m'aider à «comprendre» et voient comment cela m'aidera à écrire un code plus robuste, mais ... je ne peux m'empêcher de penser que ce sont eux qui ne comprennent pas. .

Est-il raisonnable pour une norme de codage d'exiger que chaque pointeur déréférencé dans une fonction soit vérifié en NULLpremier, même pour les données privées? (Remarque: pour donner un certain contexte, nous fabriquons un dispositif électronique grand public, et non un système de contrôle du trafic aérien ou un autre produit «échec = égalité de personnes».)

EDIT : Dans l'exemple ci-dessus, le msgSender_collaborateur n'est pas facultatif. Si c'est le cas NULL, cela indique un bug. La seule raison pour laquelle il est passé au constructeur est qu'il PowerManagerpeut être testé avec une IMsgSendersous-classe fictive .

SOMMAIRE : Il y avait de très bonnes réponses à cette question, merci à tous. J'ai accepté celui de @aaronps principalement en raison de sa brièveté. Il semble y avoir un accord général assez large sur ce qui suit:

  1. Mandater des NULLgardes pour chaque pointeur déréférencé est excessif, mais
  2. Vous pouvez éviter tout le débat en utilisant une référence (si possible) ou un constpointeur, et
  3. assertles instructions sont une alternative plus éclairée que les NULLgardes pour vérifier que les conditions préalables d'une fonction sont remplies.
échapper
la source
14
Ne pensez pas une minute que les erreurs sont du domaine des programmeurs débutants. 95% des développeurs, quel que soit leur niveau d’expérience, ont quelque chose en mal de temps en temps. Les 5% restants mentent entre leurs dents.
Blrfl
56
Si je voyais quelqu'un écrire du code qui vérifie la nullité et qui échoue en silence, je voudrais le renvoyer sur-le-champ.
Winston Ewert
16
Stuff échouer silencieusement est à peu près le pire. Au moins, quand il explose, vous savez où l'explosion se produit. Vérifier nullet ne rien faire n'est qu'un moyen de déplacer le bogue vers le bas de l'exécution, ce qui rend beaucoup plus difficile le suivi de la source.
MirroredFate
1
+1, question très intéressante. En plus de ma réponse, je ferais également remarquer que lorsque vous devez écrire du code qui a pour but de permettre un test unitaire, vous faites réellement affaire à un risque accru de faux sentiment de sécurité (plus de lignes de code = plus de possibilités pour les bugs). Reconcevoir pour utiliser des références améliore non seulement la lisibilité du code, mais réduit également la quantité de code (et de tests) que vous devez écrire pour prouver que cela fonctionne.
Seth
6
@Rig, je suis sûr qu'il existe des cas appropriés pour un échec silencieux. Mais si quelqu'un met silencieux échoue dans la pratique comme générale ( à moins de travailler dans un domaine où il logique), je ne veux pas travailler sur un projet qu'ils mettent code.
Winston Ewert

Réponses:

66

Cela dépend du "contrat":

Si PowerManager DOIT avoir un valide IMsgSender, ne jamais vérifier la valeur null, laissez-le mourir plus tôt.

Si en revanche, il PEUT avoir un IMsgSender, alors vous devez vérifier chaque fois que vous utilisez, aussi simple que cela.

Dernier commentaire sur l'histoire du programmeur junior, le problème est en réalité le manque de procédures de test.

aaronps
la source
4
+1 pour une réponse très succincte qui résume assez bien ce que je ressens: "ça dépend ...". Vous avez également eu le courage de dire "ne jamais vérifier la valeur null" ( si la valeur null est invalide). Placer une fonction assert()dans chaque membre qui supprime un pointeur est un compromis qui apaisera probablement beaucoup de monde, mais même cela me semble être une "paranoïa spéculative". La liste des choses au-delà de ma capacité de contrôle est infinie et une fois que je me permets de m'inquiéter pour elles, cela devient une pente vraiment glissante. En apprenant à faire confiance à mes tests, mes collègues et mon débogueur m'ont aidé à mieux dormir la nuit.
evadeflow
2
Lorsque vous lisez du code, ces assertions peuvent être très utiles pour comprendre comment le code est censé fonctionner. Je n'ai jamais rencontré de code source qui m'ait obligé à aller «J'aurais aimé qu'il n'y ait pas toutes ces affirmations partout», mais j'en ai vu beaucoup où j'ai dit le contraire.
Kristof Provost
1
En clair, c'est la réponse correcte à la question.
Matthew Azkimov
2
C'est la réponse correcte la plus proche, mais je ne peux pas accepter «Ne jamais vérifier la valeur de zéro», vérifiez toujours les paramètres non valides au moment où ils seront affectés à une variable membre.
James
Merci pour les commentaires. Il est préférable de le laisser planter plus tôt si quelqu'un ne lit pas la spécification et l'initialise avec la valeur null lorsqu'il doit y avoir quelque chose de valide.
aaronps
77

Je pense que le code devrait se lire comme suit:

PowerManager::PowerManager(IMsgSender* msgSender)
  : msgSender_(msgSender)
{
    assert(msgSender);
}

void PowerManager::SignalShutdown()
{
    assert(msgSender_);
    msgSender_->sendMsg("shutdown()");
}

C’est mieux que de garder la valeur NULL, car il est très clair que la fonction ne doit jamais être appelée si elle msgSender_est NULL. Il s'assure également que vous remarquerez si cela se produit.

La "sagesse" partagée par vos collègues ignorera silencieusement cette erreur, avec des résultats imprévisibles.

En général, les bogues sont plus faciles à corriger s’ils sont détectés plus près de leur cause. Dans cet exemple, la protection NULL proposée mènerait à un message d'arrêt non défini, ce qui pourrait entraîner un bogue visible ou non. Vous auriez plus de difficulté à revenir à la SignalShutdownfonction que si l’application entière venait de mourir, produisant un backtrace ou un core dump très utile, pointant directement vers SignalShutdown().

C'est un peu contre-intuitif, mais planter dès que quelque chose ne va pas a tendance à rendre votre code plus robuste. C'est parce que vous trouvez réellement les problèmes et que les causes sont également très évidentes.

Kristof Provost
la source
10
La grande différence entre appeler assert et l'exemple de code de l'OP réside dans le fait qu'assert n'est activé que dans les versions de débogage (#define NDEBUG). Lorsque le produit arrive entre les mains du client et que le débogage est désactivé, une combinaison ininterrompue de chemins de code s'exécute le pointeur null même si la fonction en question est exécutée, l'assertion ne vous fournit aucune protection.
Atk
17
Vous avez probablement testé la version avant de l'envoyer au client, mais oui, il s'agit d'un risque possible. Les solutions sont toutefois simples: soit vous envoyez les assertions activées (c'est la version que vous avez de toute façon testées), soit vous les remplacez par votre propre macro qui affirme en mode débogage et ne consomme que les journaux, les journaux + les traces, ... en mode de publication.
Kristof Provost
7
@ James - dans 90% des cas, vous ne récupérerez pas non plus de la vérification NULL échouée. Mais dans ce cas, le code échouera de manière silencieuse et une erreur peut apparaître beaucoup plus tard - par exemple, vous pensiez que vous aviez sauvegardé dans un fichier, mais en réalité, la vérification nulle échouait parfois, ce qui ne vous laissait pas le plus sage. Vous ne pouvez pas récupérer de toute situation qui ne doit pas se produire. Vous pouvez vérifier que cela n'arrivera pas, mais je ne pense pas que vous ayez un budget à moins d'écrire du code pour un satellite de la NASA ou une centrale nucléaire. Vous devez avoir un moyen de dire "Je n'ai aucune idée de ce qui se passe - le programme n'est pas censé être dans cet état".
Maciej Piechotka
6
@ James je suis en désaccord avec jeter. Cela donne à penser que c'est quelque chose qui peut réellement arriver. Les affirmations concernent des choses supposées impossibles. Pour de vrais bugs dans le code en d'autres termes. Les exceptions sont pour quand inattendu, mais possible, les choses se produisent. Les exceptions concernent les choses que vous pourriez imaginer gérer et récupérer. Les erreurs de logique dans le code que vous ne pouvez pas récupérer, et vous ne devriez pas essayer non plus.
Kristof Provost
2
@ James: D'après mon expérience des systèmes embarqués, les logiciels et le matériel sont invariablement conçus de telle sorte qu'il est extrêmement difficile de rendre un périphérique complètement inutilisable. Dans la grande majorité des cas, il existe un chien de garde matériel qui force une réinitialisation complète du périphérique si le logiciel cesse de fonctionner.
Bart van Ingen Schenau
30

Si msgSender ne doit jamais l' être null, vous devez placer la nullvérification uniquement dans le constructeur. Ceci est également vrai pour toutes les autres entrées de la classe - placez le contrôle d'intégrité au point d'entrée dans le 'module' - classe, fonction, etc.

Ma règle empirique consiste à effectuer des contrôles d'intégrité entre les limites des modules - la classe dans ce cas. En outre, une classe doit être suffisamment petite pour qu'il soit possible de vérifier rapidement mentalement l'intégrité de la durée de vie des membres de la classe, ce qui garantit que des erreurs telles que des suppressions incorrectes / assignations nulles sont évitées. La vérification null effectuée dans le code de votre publication suppose que toute utilisation non valide affecte en réalité null au pointeur - ce qui n'est pas toujours le cas. Dans la mesure où «utilisation non valide» implique intrinsèquement que toute hypothèse concernant le code standard ne s'applique pas, nous ne pouvons pas être sûrs d'attraper tous les types d'erreurs de pointeur - par exemple une suppression non valide, une incrémentation, etc.

De plus, si vous êtes certain que l'argument ne peut jamais être null, envisagez d'utiliser des références, en fonction de votre utilisation de la classe. Sinon, envisagez d'utiliser std::unique_ptrou std::shared_ptrau lieu d'un pointeur brut.

Max
la source
11

Non, il n’est pas raisonnable de vérifier chaque déréférence de pointeur pour le pointeur NULL.

Les vérifications de points nuls sont utiles sur les arguments de fonction (y compris les arguments de constructeur) pour garantir le respect des conditions préalables ou pour prendre les mesures appropriées si un paramètre facultatif n'est pas fourni. Elles sont également utiles pour vérifier l'invariant d'une classe après avoir exposé les éléments internes de la classe. Mais si la seule raison pour laquelle un pointeur est devenu NULL est l'existence d'un bogue, alors la vérification est inutile. Ce bogue aurait tout aussi bien pu définir le pointeur sur une autre valeur non valide.

Si j'étais confronté à une situation comme la vôtre, je poserais deux questions:

  • Pourquoi l'erreur de ce programmeur junior n'a-t-elle pas été découverte avant la sortie? Par exemple dans une révision de code ou dans la phase de test? La mise sur le marché d'un appareil électronique grand public défectueux peut être tout aussi coûteuse (si vous prenez en compte la part de marché et la bonne volonté) que de libérer un appareil défectueux utilisé dans une application critique pour la sécurité. Je m'attendrais donc à ce que l'entreprise soit sérieuse en matière de test autres activités d'AQ.
  • Si le contrôle nul échoue, quel traitement d'erreur voulez-vous que je mette en place ou pourrais-je simplement écrire à la assert(msgSender_)place du contrôle nul? Si vous venez de mettre la vérification à zéro, vous avez peut-être évité un blocage, mais vous avez peut-être créé une situation pire car le logiciel continue sur la prémisse qu'une opération a eu lieu alors qu'en réalité, cette opération a été ignorée. Cela pourrait rendre d'autres parties du logiciel instables.
Bart van Ingen Schenau
la source
9

Cet exemple semble porter davantage sur la durée de vie d'un objet que sur la question de savoir si un paramètre d'entrée est null †. Puisque vous mentionnez que le PowerManagerdoit toujours avoir une valeur valide IMsgSender, passer l'argument par pointeur (permettant ainsi la possibilité d'un pointeur nul) me semble être un défaut de conception ††.

Dans de telles situations, je préférerais modifier l'interface pour que les exigences de l'appelant soient respectées par le langage:

PowerManager::PowerManager(const IMsgSender& msgSender)
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    msgSender_->sendMsg("shutdown()");
}

En le réécrivant de cette manière, il est PowerManagernécessaire de conserver une référence IMsgSenderpendant toute sa durée de vie. Ceci, à son tour, établit également une exigence implicite qui IMsgSenderdoit vivre plus longtemps PowerManageret annule la nécessité de toute vérification de pointeur null ou d'assertions à l'intérieur PowerManager.

Vous pouvez également écrire la même chose en utilisant un pointeur intelligent (via boost ou c ++ 11), pour forcer explicitement IMsgSenderà vivre plus longtemps que PowerManager:

PowerManager::PowerManager(std::shared_ptr<IMsgSender> msgSender) 
  : msgSender_(msgSender) {}

void PowerManager::SignalShutdown() {
    // Here, we own a smart pointer to IMsgSender, so even if the caller
    // destroys the original pointer, we still have a valid copy
    msgSender_->sendMsg("shutdown()");
}

Cette méthode est préférable s’il est possible que sa IMsgSenderdurée de vie ne puisse être garantie supérieure à celle PowerManagerde (s x = new IMsgSender(); p = new PowerManager(*x);).

† En ce qui concerne les pointeurs: le contrôle nul généralisé rend le code plus difficile à lire et n'améliore pas la stabilité (il améliore l' apparence de la stabilité, ce qui est bien pire).

Quelque part, quelqu'un a une adresse mémoire pour tenir le IMsgSender. Il incombe à cette fonction de s’assurer que l’allocation a réussi (vérification des valeurs renvoyées par la bibliothèque ou gestion correcte des std::bad_allocexceptions), afin de ne pas transmettre de pointeurs non valides.

Puisque le PowerManagerne possède pas le IMsgSender(c'est juste l'emprunter pour un moment), il n'est pas responsable de l'allocation ou de la destruction de cette mémoire. C'est une autre raison pour laquelle je préfère la référence.

†† Puisque vous êtes nouveau dans cet emploi, je suppose que vous piratez du code existant. Donc, par faille de conception, je veux dire que la faille se trouve dans le code avec lequel vous travaillez. Ainsi, les personnes qui signalent votre code car il ne vérifie pas les pointeurs nuls s’authentifient elles-mêmes pour écrire du code nécessitant des pointeurs :)

Seth
la source
1
En fait, il n’ya parfois rien de mal à utiliser des pointeurs bruts. Std :: shared_ptr n'est pas une solution miracle et son utilisation dans une liste d'arguments est un remède contre une ingénierie bâclée, bien que je sache qu'il est considéré comme un prétexte pour l'utiliser autant que possible. Utilisez java si vous vous sentez comme ça.
James
2
Je n'ai pas dit que les pointeurs bruts sont mauvais! Ils ont définitivement leur place. Cependant, ceci est C + + , les références (pointeurs et partagés, maintenant) font partie de la langue pour une raison. Utilisez-les, ils vous feront réussir.
Seth
J'aime l'idée du pointeur intelligent, mais ce n'est pas une option dans ce concert en particulier. +1 pour recommander l'utilisation de références (comme @Max et quelques autres l'ont). Parfois , il est impossible d'injecter une référence, cependant, en raison de problèmes de dépendance de la poule et l' œuf, à savoir, si un objet qui serait autrement un candidat pour l' injection doit avoir un pointeur (ou référence) à son titulaire injecté dans ce . Cela peut parfois indiquer une conception à couplage étroit; mais c'est souvent acceptable, comme dans la relation entre un objet et son itérateur, où il n'est jamais logique d'utiliser le dernier sans le premier.
evadeflow
8

Comme les exceptions, les conditions de garde ne sont utiles que si vous savez quoi faire pour remédier à l'erreur ou si vous souhaitez transmettre un message d'exception plus significatif.

En avalant une erreur (qu'il s'agisse d'une exception ou d'un contrôle de sécurité), ce n'est que la bonne chose à faire lorsque l'erreur n'a pas d'importance. L'endroit le plus courant pour que je voie les erreurs avalées est dans le code de journalisation des erreurs. Vous ne voulez pas bloquer une application, car vous ne parvenez pas à enregistrer un message d'état.

Chaque fois qu'une fonction est appelée, et que ce n'est pas un comportement optionnel, elle devrait échouer bruyamment, pas en silence.

Edit: en pensant à votre histoire de programmeur junior, il semblerait que ce qui s’est passé est qu’un membre privé a été mis à null alors que cela n’était jamais supposé être autorisé. Ils ont eu un problème d'écriture inappropriée et tentent de le corriger en validant à la lecture. C'est à l'envers. Au moment où vous l'identifiez, l'erreur est déjà survenue. La solution exécutable de révision de code / compilateur pour cela n'est pas une condition de garde, mais plutôt des getters et des setters ou des membres const.

Jmoreno
la source
7

Comme d' autres l' ont noté, cela dépend si oui ou non msgSenderpeut être légitimement NULL . Ce qui suit suppose que cela ne devrait jamais être NULL.

void PowerManager::SignalShutdown()
{
    if (!msgSender_)
    {
       throw SignalException("Shut down failed because message sender is not set.");
    }

    msgSender_->sendMsg("shutdown()");
}

Le «correctif» proposé par les autres membres de votre équipe enfreint le principe Dead Programmes Tell No Lies . Les bugs sont vraiment difficiles à trouver tels quels. Une méthode qui modifie silencieusement son comportement en fonction d'un problème précédent rend non seulement difficile la recherche du premier bogue, mais ajoute également un deuxième bogue qui lui est propre.

La junior a fait des ravages en ne vérifiant pas la nullité. Que se passe-t-il si ce morceau de code fait des ravages en continuant à s'exécuter dans un état non défini (le périphérique est activé mais le programme "pense" qu'il est désactivé)? Une autre partie du programme fera peut-être quelque chose de sûr uniquement lorsque l'appareil est éteint.

L'une ou l'autre de ces approches évitera les échecs silencieux:

  1. Utilisez les assertions suggérées par cette réponse , mais assurez-vous qu'elles sont activées dans le code de production. Ceci, bien sûr, pourrait poser problème si d’autres assertions étaient rédigées dans l’hypothèse où elles seraient hors production.

  2. Lance une exception si elle est nulle.

poussée
la source
5

Je suis d'accord avec piéger le null dans le constructeur. De plus, si le membre est déclaré dans l'en-tête en tant que:

IMsgSender* const msgSender_;

Ensuite, le pointeur ne peut plus être modifié après l’initialisation. Par conséquent, si la configuration est correcte, la durée de vie de l’objet le contenant sera satisfaisante. (L'objet pointé à ne pas être const.)

Grimm l'Opiner
la source
C'est un bon conseil, merci! Tellement bon, en fait, que je suis désolé, je ne peux pas le voter plus haut. Ce n'est pas une réponse directe à la question posée, mais c'est un excellent moyen d'éliminer toute possibilité que le pointeur devienne «accidentellement» NULL.
evadeflow
@evadeflow Je pensais que "je suis d'accord avec tout le monde" a répondu à l'essentiel de la question. Vive bien! ;-)
Grimm The Opiner
Il serait un peu impoli de ma part de changer la réponse acceptée à ce stade, compte tenu de la façon dont la question a été formulée. Mais je pense vraiment que ce conseil est exceptionnel et je suis désolé de ne pas y avoir pensé moi-même. Avec un constpointeur, nul besoin de assert()sortir du constructeur. Cette réponse semble même un peu meilleure que celle de @Kristof Provost (qui était également remarquable, mais qui abordait aussi la question de manière indirecte.) J'espère que d'autres voteront en faveur de cette proposition Cela n’a vraiment aucun sens de faire partie assertde chaque méthode quand vous pouvez simplement faire le pointeur const.
evadeflow
@evadeflow Les gens ne visitent que Questins pour le représentant. Maintenant, il a été répondu que personne ne l'examinera à nouveau. ;-) Je ne suis entré que parce que c'était une question de pointeurs et je garde un œil sur la sanglante brigade "Utilisez des pointeurs intelligents pour toujours".
Grimm The Opiner
3

C'est carrément dangereux!

J'ai travaillé sous un développeur senior dans une base de code C avec les "standards" les plus médiocres qui ont poussé pour la même chose, pour vérifier aveuglément tous les pointeurs pour null. Le développeur finirait par faire des choses comme ceci:

// Pre: vertex should never be null.
void transform_vertex(Vertex* vertex, ...)
{
    // Inserted by my "wise" co-worker.
    if (!vertex)
        return;
    ...
}

Une fois, j'ai essayé de supprimer une telle vérification de la condition préalable une fois dans une telle fonction et de la remplacer par un assertpour voir ce qui se produirait.

À ma grande horreur, j'ai trouvé dans la base de code des milliers de lignes de code qui transmettaient des valeurs null à cette fonction, mais où les développeurs, probablement confus, ont contourné et simplement ajouté plus de code jusqu'à ce que les choses fonctionnent.

À ma plus grande horreur, j’ai constaté que ce problème était répandu dans toutes sortes d’endroits de la base de code en recherchant les valeurs NULL. La base de code s'était développée au fil des décennies pour pouvoir compter sur ces vérifications afin de pouvoir violer en silence les conditions préalables les plus explicitement documentées. En supprimant ces contrôles mortels en faveur de asserts, toutes les erreurs humaines logiques de la base de code sur des décennies seraient révélées et nous nous y noierions.

Il n'a fallu que deux lignes de code apparemment innocentes comme celle-ci + temps et une équipe pour masquer un millier de bogues accumulés.

C’est le genre de pratiques qui fait que les bogues dépendent d’autres bogues pour que le logiciel fonctionne. C'est un scénario de cauchemar . Cela rend également chaque erreur logique liée à la violation de telles conditions préalables afficher mystérieusement un million de lignes de code du site dans lequel l'erreur s'est produite, puisque toutes ces vérifications nulles masquent simplement le bogue et le cachent jusqu'à ce que nous arrivions à un endroit qui a oublié cacher le bug.

Vouloir simplement vérifier les nulls à l'aveuglette à chaque endroit où un pointeur nul viole une condition préalable est pour moi une totale folie, à moins que votre logiciel ne soit si critique contre les échecs d'affirmation et les arrêts de production que le potentiel de ce scénario est préférable.

Est-il raisonnable qu'une norme de codage exige que chaque pointeur déréférencé dans une fonction soit vérifié en premier pour la valeur NULL, même pour les membres de données privés?

Donc, je dirais, absolument pas. Ce n'est même pas "sûr". Cela peut très bien être le contraire et masquer toutes sortes de bugs dans votre base de code qui, au fil des années, peuvent conduire aux scénarios les plus horribles.

assertest le chemin à parcourir ici. Les violations des conditions préalables ne doivent pas passer inaperçues, sans quoi la loi de Murphy peut facilement entrer en vigueur.


la source
1
"assert" est la voie à suivre - mais rappelez-vous que sur tout système moderne, chaque accès à la mémoire via un pointeur est associé à une assertion null-pointeur intégrée au matériel. Donc, dans "assert (p! = NULL); return * p;" même l’affirmation est généralement inutile.
gnasher729
@ gnasher729 Ah, c'est un très bon point, mais j'ai tendance à voir assertà la fois un mécanisme de documentation permettant d'établir les conditions préalables et non pas simplement un moyen de forcer un avortement. Mais j'aime aussi un peu la nature de l'échec de l'assertion qui vous indique exactement quelle ligne de code a provoqué un échec et la condition d'assertion a échoué ("documentation de l'accident"). Peut s'avérer utile si nous finissons par utiliser une version de débogage en dehors d'un débogueur et que nous sommes pris au dépourvu.
@ gnasher729 Ou j'ai peut-être mal compris une partie de celle-ci. Ces systèmes modernes montrent-ils quelle ligne de code source dans laquelle l'assertion a échoué? J'imagine généralement qu'ils ont perdu l'information à ce moment-là et qu'ils pourraient ne montrer que des violations de segfault / accès, mais je suis un peu loin d'être "moderne" - toujours obstinément sous Windows 7 pour l'essentiel de mon développement.
Par exemple, sous MacOS X et iOS, si votre application se bloque en raison d'un accès nul au pointeur pendant le développement, le débogueur vous indiquera la ligne de code exacte où cela se produit. Il existe un autre aspect étrange: le compilateur essaiera de vous avertir si vous faites quelque chose de suspect. Si vous passez un pointeur sur une fonction et y accédez, le compilateur ne vous avertira pas que le pointeur peut être NULL, car cela se produit trop souvent, vous risqueriez de vous noyer. Cependant, si vous faites une comparaison si (p == NULL) ... alors le compilateur suppose qu'il y a de bonnes chances que p soit NULL,
gnasher729
parce que pourquoi voudriez-vous l'avoir testé sinon, il vous avertira dès lors si vous l'utilisez sans test. Si vous utilisez votre propre macro de type assert, vous devez être un peu malin pour l'écrire de manière à ce que "mon_assert (p! = NULL," c'est un pointeur nul, stupide! "); * P = 1;" ne vous avertit pas.
gnasher729
2

Objective-C , par exemple, traite chaque appel de méthode sur un nilobjet comme une opération non opérante dont la valeur est zéro. Cette décision de conception dans Objective-C présente certains avantages, pour les raisons évoquées dans votre question. Le concept théorique de garder chaque appel de méthode avec une protection nulle a un certain mérite s'il est bien annoncé et appliqué de manière cohérente.

Cela dit, le code vit dans un écosystème, pas dans un vide. Le comportement sans protection serait non idiomatique et surprenant en C ++, et devrait donc être considéré comme nuisible. En résumé, personne n'écrit le code C ++ de cette façon, alors ne le faites pas ! À titre de contre-exemple, cependant, notez que l'appel free()ou deletesur un NULLen C et C ++ est garanti d'être un non-op.


Dans votre exemple, il vaudrait probablement la peine de placer une assertion msgSendernon nulle dans le constructeur . Si le constructeur appelle une méthode sur msgSendertout de suite, alors une telle affirmation ne serait nécessaire, car il plantait là de toute façon. Toutefois, comme il ne s'agit que de stocker msgSender pour une utilisation future, il ne serait pas évident d'examiner la trace d'une pile SignalShutdown()indiquant comment la valeur a été créée NULL. Une assertion dans le constructeur faciliterait considérablement le débogage.

Mieux encore, le constructeur devrait accepter une const IMsgSender&référence, ce qui est impossible NULL.

200_success
la source
1

La raison pour laquelle vous êtes invité à éviter les déréférences NULL est de vous assurer que votre code est robuste. Les exemples de programmeurs débutants il y a très longtemps ne sont que des exemples. N'importe qui peut rompre le code par accident et provoquer une déréférence nulle, en particulier pour les globals et les globals de classe. En C et C ++, c'est encore plus possible accidentellement, avec la capacité de gestion directe de la mémoire. Vous pourriez être surpris, mais ce genre de chose arrive très souvent. Même par des développeurs très compétents, très expérimentés et très expérimentés.

Vous n'avez pas besoin de tout vérifier, mais vous devez vous protéger contre les déréférences ayant une probabilité décente d'être nul. C’est généralement lorsqu’ils sont attribués, utilisés et déréférencés dans différentes fonctions. Il est possible qu'une des autres fonctions soit modifiée et casse votre fonction. Il est également possible qu'une des autres fonctions soit appelée hors service (comme si vous avez un désallocateur pouvant être appelé séparément du destructeur).

Je préfère l’approche que vos collègues vous disent en combinaison avec assert. Crash dans l'environnement de test, il est donc plus évident qu'il existe un problème à résoudre et à échouer en production.

Vous devriez également utiliser un outil de correction de code robuste tel que la couverture ou la fortification. Et vous devriez adresser tous les avertissements du compilateur.

Edit: comme d’autres l’ont déjà mentionné, une défaillance silencieuse, comme dans plusieurs exemples de code, est également généralement la mauvaise solution. Si votre fonction ne peut pas récupérer la valeur null, elle doit renvoyer une erreur (ou une exception) à son appelant. L’appelant est responsable de la correction de son ordre d’appel, de la récupération ou du renvoi d’une erreur (ou du lancement d’une exception) à son appelant, etc. En fin de compte, une fonction est capable de récupérer et de poursuivre en douceur, puis de récupérer et d'échouer (par exemple, une base de données échouant sur une transaction en raison d'une erreur interne d'un utilisateur mais ne se terminant pas automatiquement), ou la fonction détermine que l'état de l'application est corrompu. et irrécupérable et l'application se ferme.

atk
la source
+1 pour avoir eu le courage de soutenir une position (apparemment) impopulaire. J'aurais été déçu si personne n'avait défendu mes collègues à ce sujet (ce sont des personnes très intelligentes qui produisent un produit de qualité depuis de nombreuses années). J'aime aussi l'idée que les applications devraient "planter dans l'environnement de test ... et échouer en production." Je ne suis pas convaincue que l'exigence de NULLvérifier par cœur est la meilleure façon de le faire, mais j'apprécie d'entendre quelqu'un de l'extérieur de mon lieu de travail qui dit essentiellement: "Aigh. Cela ne semble pas trop déraisonnable."
evadeflow
Je suis ennuyé quand je vois des gens tester NULL après malloc (). Cela me dit qu'ils ne comprennent pas comment les systèmes d'exploitation modernes gèrent la mémoire.
Kristof Provost
2
Mon hypothèse est que @KristofProvost parle des systèmes d’exploitation qui utilisent la surcommission, pour lesquels malloc réussit toujours (mais le système d’exploitation peut ultérieurement tuer votre processus s’il n’ya pas assez de mémoire disponible). Toutefois, ce n'est pas une bonne raison pour ignorer les vérifications nulles sur malloc: la sur-utilisation n'est pas universelle sur toutes les plateformes et même si elle est activée, il peut y avoir d'autres raisons pour que malloc échoue (par exemple, sous Linux, une limite d'espace d'adressage de processus définie avec ulimit ).
John Bartholomew
1
Ce que John a dit. Je parlais en effet de surconsommation. Overcommit est activé par défaut sous Linux (c'est ce qui paie mes factures). Je ne sais pas, mais suspecte donc, si c'est la même chose sous Windows. De toute façon, dans la plupart des cas, vous allez finir par planter, sauf si vous êtes prêt à écrire beaucoup de code qui ne sera jamais testé pour gérer des erreurs qui ne se produiront certainement pas ...
Kristof Provost
2
Laissez-moi ajouter que je n’ai jamais non plus vu de code (en dehors du noyau Linux, où le manque de mémoire est pratiquement possible) qui gérera correctement une condition de manque de mémoire. De toute façon, tout le code que j'ai vu essayer s'effondrerait plus tard. Tout ce qui a été accompli a été de perdre du temps, de rendre le code plus difficile à comprendre et de cacher le véritable problème. Les déréférences Null sont faciles à déboguer (si vous avez un fichier core).
Kristof Provost
1

L'utilisation d'un pointeur au lieu d'une référence me dirait qu'il nemsgSender s'agit que d'une option et qu'ici le contrôle nul serait correct. L'extrait de code est trop lent pour pouvoir en décider. Peut-être y a-t-il d'autres éléments PowerManagerqui sont précieux (ou testables) ...

Lorsque je choisis entre pointeur et référence, j’ai bien pesé les deux options. Si je dois utiliser un pointeur pour un membre (même pour les membres privés), je dois accepter la if (x)procédure à chaque fois que je la déréférence.

Loup
la source
Comme je l'ai mentionné dans un autre commentaire quelque part, il est parfois impossible d' injecter une référence . Un exemple que je rencontre
souvent
@evadeflow: OK, je l'avoue, cela dépend de la taille de la classe et de la visibilité des membres du pointeur (il est impossible de faire référence), que je vérifie avant le déréférencement. Dans les cas où la classe est petite et claire et que le ou les pointeurs sont privés, je ne ...
Wolf
0

Cela dépend de ce que vous voulez arriver.

Vous avez écrit que vous travaillez sur "un appareil électronique grand public". Si, en quelque sorte, un bug est introduit en définissant msgSender_sur NULL, voulez-vous

  • le dispositif pour continuer, en sautant SignalShutdownmais en continuant le reste de son fonctionnement, ou
  • l'appareil tombe en panne, obligeant l'utilisateur à le redémarrer?

Selon l'impact du non-envoi du signal d'arrêt, l'option 1 pourrait être un choix viable. Si l'utilisateur peut continuer à écouter sa musique, mais que l'écran affiche toujours le titre de la piste précédente, cela peut être préférable à un crash complet de l'appareil.

Bien sûr, si vous choisissez l'option 1, un élémentassert (tel que recommandé par d'autres) est essentiel pour réduire la probabilité qu'un tel bogue se glisse inaperçu au cours du développement. La ifprotection nulle est uniquement là pour atténuer les défaillances lors de l'utilisation en production.

Personnellement, je préfère également l'approche "crash early" pour les versions de production, mais je développe des logiciels de gestion qui peuvent être facilement corrigés et mis à jour en cas de bogue. Pour les appareils électroniques grand public, cela n’est peut-être pas si facile.

Heinzi
la source