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 NULL
garde 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 NULL
ensuite à , 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 NULL
vé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 NULL
premier, 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 PowerManager
peut être testé avec une IMsgSender
sous-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:
- Mandater des
NULL
gardes pour chaque pointeur déréférencé est excessif, mais - Vous pouvez éviter tout le débat en utilisant une référence (si possible) ou un
const
pointeur, et assert
les instructions sont une alternative plus éclairée que lesNULL
gardes pour vérifier que les conditions préalables d'une fonction sont remplies.
la source
null
et 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.Réponses:
Cela dépend du "contrat":
Si
PowerManager
DOIT avoir un valideIMsgSender
, 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.
la source
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.Je pense que le code devrait se lire comme suit:
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
SignalShutdown
fonction que si l’application entière venait de mourir, produisant un backtrace ou un core dump très utile, pointant directement versSignalShutdown()
.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.
la source
Si msgSender ne doit jamais l' être
null
, vous devez placer lanull
vé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_ptr
oustd::shared_ptr
au lieu d'un pointeur brut.la source
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:
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.la source
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
PowerManager
doit toujours avoir une valeur valideIMsgSender
, 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:
En le réécrivant de cette manière, il est
PowerManager
nécessaire de conserver une référenceIMsgSender
pendant toute sa durée de vie. Ceci, à son tour, établit également une exigence implicite quiIMsgSender
doit vivre plus longtempsPowerManager
et annule la nécessité de toute vérification de pointeur null ou d'assertions à l'intérieurPowerManager
.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 quePowerManager
:Cette méthode est préférable s’il est possible que sa
IMsgSender
durée de vie ne puisse être garantie supérieure à cellePowerManager
de (sx = 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 desstd::bad_alloc
exceptions), afin de ne pas transmettre de pointeurs non valides.Puisque le
PowerManager
ne possède pas leIMsgSender
(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 :)
la source
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.
la source
Comme d' autres l' ont noté, cela dépend si oui ou non
msgSender
peut être légitimementNULL
. Ce qui suit suppose que cela ne devrait jamais être NULL.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:
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.
Lance une exception si elle est nulle.
la source
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:
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.)
la source
NULL
.const
pointeur, nul besoin deassert()
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 partieassert
de chaque méthode quand vous pouvez simplement faire le pointeurconst
.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:
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
assert
pour 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.
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.
assert
est 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
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.Objective-C , par exemple, traite chaque appel de méthode sur un
nil
objet 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()
oudelete
sur unNULL
en C et C ++ est garanti d'être un non-op.Dans votre exemple, il vaudrait probablement la peine de placer une assertion
msgSender
non nulle dans le constructeur . Si le constructeur appelle une méthode surmsgSender
tout 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 stockermsgSender
pour une utilisation future, il ne serait pas évident d'examiner la trace d'une pileSignalShutdown()
indiquant comment la valeur a été crééeNULL
. 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 impossibleNULL
.la source
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.
la source
NULL
vé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."L'utilisation d'un pointeur au lieu d'une référence me dirait qu'il ne
msgSender
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émentsPowerManager
qui 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.la source
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_
surNULL
, voulez-vousSignalShutdown
mais en continuant le reste de son fonctionnement, ouSelon 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ément
assert
(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. Laif
protection 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.
la source