GCC 6 a une nouvelle fonctionnalité d'optimisation : il suppose que ce this
n'est toujours pas nul et optimise en fonction de cela.
La propagation de la plage de valeurs suppose désormais que le pointeur this des fonctions membres C ++ est non nul. Cela élimine les vérifications de pointeurs nulles courantes mais casse également certaines bases de code non conformes (telles que Qt-5, Chromium, KDevelop) . Pour contourner le problème, vous pouvez utiliser -fno-delete-null-pointer-checks. Un code incorrect peut être identifié en utilisant -fsanitize = undefined.
Le document de modification appelle clairement cela comme dangereux car il rompt une quantité surprenante de code fréquemment utilisé.
Pourquoi cette nouvelle hypothèse casserait-elle le code C ++ pratique?Existe-t-il des schémas particuliers où des programmeurs imprudents ou non informés s'appuient sur ce comportement particulier non défini? Je ne peux pas imaginer que quiconque écrive if (this == NULL)
parce que ce n'est pas naturel.
la source
this
est passé comme un paramètre implicite, alors ils commencent à l'utiliser comme s'il s'agissait d'un paramètre explicite. Ce n'est pas. Lorsque vous déréférencez un null this, vous invoquez UB comme si vous déréférençiez tout autre pointeur null. C'est tout ce qu'il y a à faire. Si vous souhaitez transmettre des nullptrs, utilisez un paramètre explicite, DUH . Ce ne sera pas plus lent, ce ne sera pas plus maladroit, et le code qui a une telle API est de toute façon profondément dans les internes, donc a une portée très limitée. Fin de l'histoire je pense.Réponses:
Je suppose que la question à laquelle il faut répondre pourquoi des gens bien intentionnés écriraient les chèques en premier lieu.
Le cas le plus courant est probablement si vous avez une classe qui fait partie d'un appel récursif naturel.
Si tu avais:
en C, vous pourriez écrire:
En C ++, c'est bien d'en faire une fonction membre:
Au début du C ++ (avant la standardisation), il était souligné que les fonctions membres étaient du sucre syntaxique pour une fonction où le
this
paramètre est implicite. Le code a été écrit en C ++, converti en C équivalent et compilé. Il y avait même des exemples explicites que la comparaisonthis
à null était significative et le compilateur Cfront d'origine en a également profité. Donc, venant d'un fond C, le choix évident pour le contrôle est:Remarque: Bjarne Stroustrup mentionne même que les règles pour
this
ont changé au fil des ans iciEt cela a fonctionné sur de nombreux compilateurs pendant de nombreuses années. Lorsque la normalisation a eu lieu, cela a changé. Et plus récemment, les compilateurs ont commencé à tirer parti de l'appel d'une fonction membre où
this
êtrenullptr
est un comportement indéfini, ce qui signifie que cette condition est toujoursfalse
et que le compilateur est libre de l'omettre.Cela signifie que pour effectuer une traversée de cet arbre, vous devez soit:
Faites toutes les vérifications avant d'appeler
traverse_in_order
Cela signifie également vérifier sur CHAQUE site d'appel si vous pouvez avoir une racine nulle.
N'utilisez pas de fonction membre
Cela signifie que vous écrivez l'ancien code de style C (peut-être en tant que méthode statique) et que vous l'appelez avec l'objet explicitement en tant que paramètre. par exemple. vous êtes de retour à l'écriture
Node::traverse_in_order(node);
plutôt quenode->traverse_in_order();
sur le site d'appel.Je pense que le moyen le plus simple / le plus soigné de corriger cet exemple particulier d'une manière conforme aux normes est d'utiliser en fait un nœud sentinelle plutôt qu'un
nullptr
.Aucune des deux premières options ne semble aussi attrayante, et bien que le code puisse s'en tirer, ils ont écrit du mauvais code avec
this == nullptr
au lieu d'utiliser un correctif approprié.Je suppose que c'est ainsi que certaines de ces bases de code ont évolué pour avoir des
this == nullptr
vérifications.la source
1 == 0
être un comportement indéfini? C'est tout simplementfalse
.this == nullptr
idiome est un comportement indéfini car vous avez appelé une fonction membre sur un objet nullptr avant cela, ce qui n'est pas défini. Et le compilateur est libre d'omettre la vérificationthis
jamais être nullable. J'imagine que c'est peut-être l'avantage d'apprendre le C ++ à une époque où le SO existe pour ancrer les dangers d'UB dans mon cerveau et me dissuader de faire des hacks bizarres comme celui-ci.Il le fait parce que le code "pratique" était cassé et impliquait un comportement indéfini au départ. Il n'y a aucune raison d'utiliser une valeur nulle
this
, autre que comme une micro-optimisation, généralement très prématurée.C'est une pratique dangereuse, car l' ajustement des pointeurs en raison de la traversée de la hiérarchie des classes peut transformer un null
this
en un non nul. Donc, à tout le moins, la classe dont les méthodes sont censées fonctionner avec un nullthis
doit être une classe finale sans classe de base: elle ne peut dériver de rien, et elle ne peut pas être dérivée. Nous passons rapidement de la pratique au laide-hack-land .Concrètement, le code n'a pas à être moche:
Si l'arborescence est vide, c'est-à-dire null
Node* root
, vous n'êtes pas censé appeler de méthodes non statiques dessus. Période. C'est parfaitement bien d'avoir un code arborescent de type C qui prend un pointeur d'instance par un paramètre explicite.L'argument ici semble se résumer à la nécessité d'écrire des méthodes non statiques sur des objets qui pourraient être appelés à partir d'un pointeur d'instance nul. Il n'y a pas un tel besoin. La manière d'écrire un tel code en C-avec-objets est toujours plus agréable dans le monde C ++, car elle peut au moins être de type sécurisé. Fondamentalement, le nul
this
est une telle micro-optimisation, avec un champ d'utilisation si étroit, que l'interdire est parfaitement bien à mon humble avis. Aucune API publique ne doit dépendre d'une valeur nullethis
.la source
this
chèques sont sélectionnés par divers analyseurs de code statique, ce n'est donc pas comme si quelqu'un devait tous les rechercher manuellement. Le patch serait probablement quelques centaines de lignes de changements insignifiants.this
déréférencement nul est un crash instantané. Ces problèmes seront découverts très rapidement même si personne ne se soucie d'exécuter un analyseur statique sur le code. C / C ++ suit le mantra «payer uniquement pour les fonctionnalités que vous utilisez». Si vous voulez des vérifications, vous devez être explicite à leur sujet et cela signifie ne pas les fairethis
, quand il est trop tard, car le compilateur suppose qu'ilthis
n'est pas nul. Sinon, il faudrait vérifierthis
, et pour 99,9999% du code, ces vérifications sont une perte de temps.Le document ne l'appelle pas dangereux. Il ne prétend pas non plus qu'il casse une quantité surprenante de code . Il souligne simplement quelques bases de code populaires dont il prétend être connu pour s'appuyer sur ce comportement non défini et qui se briseraient en raison du changement à moins que l'option de contournement ne soit utilisée.
Si le code C ++ pratique repose sur un comportement non défini, les modifications apportées à ce comportement non défini peuvent le casser. C'est pourquoi UB doit être évité, même lorsqu'un programme qui en dépend semble fonctionner comme prévu.
Je ne sais pas si c'est un anti- modèle largement répandu , mais un programmeur non informé pourrait penser qu'il peut réparer son programme de planter en faisant:
Lorsque le bogue réel déréférence un pointeur nul ailleurs.
Je suis sûr que si le programmeur n'est pas suffisamment informé, il pourra proposer des (anti) modèles plus avancés qui reposent sur cet UB.
Je peux.
la source
if(this == null) PrintSomeHelpfulDebugInformationAboutHowWeGotHere();
Comme un joli journal facile à lire d'une séquence d'événements dont un débogueur ne peut pas facilement vous parler. Amusez-vous à déboguer maintenant sans passer des heures à placer des vérifications partout quand il y a un null aléatoire soudain dans un grand ensemble de données, dans du code que vous n'avez pas écrit ... Et la règle UB à ce sujet a été faite plus tard, après la création de C ++. C'était valide.-fsanitize=null
.-fsanitize=null
enregistrer les erreurs sur la carte SD / MMC sur les broches # 5,6,10,11 en utilisant SPI? Ce n'est pas une solution universelle. Certains ont fait valoir que l'accès à un objet nul allait à l'encontre des principes orientés objet, mais certains langages OOP ont un objet nul sur lequel il est possible de fonctionner, ce n'est donc pas une règle universelle de la POO. 1/2Une partie du code "pratique" (façon amusante d'épeler "buggy") qui a été cassé ressemblait à ceci:
et il a oublié de tenir compte du fait que
p->bar()
parfois renvoie un pointeur nul, ce qui signifie que le déréférencer pour appelerbaz()
n'est pas défini.Pas tout le code qui a été brisé contenu explicite
if (this == nullptr)
ou lesif (!p) return;
chèques. Certains cas étaient simplement des fonctions qui n'accédaient à aucune variable membre, et qui semblaient donc fonctionner correctement. Par exemple:Dans ce code, lorsque vous appelez
func<DummyImpl*>(DummyImpl*)
avec un pointeur nul, il y a un déréférencement "conceptuel" du pointeur à appelerp->DummyImpl::valid()
, mais en fait, cette fonction membre retourne simplementfalse
sans accéder*this
. Celareturn false
peut être intégré et donc, dans la pratique, le pointeur n'a pas du tout besoin d'être accédé. Donc, avec certains compilateurs, cela semble fonctionner correctement: il n'y a pas de segfault pour déréférencer null,p->valid()
est faux, donc le code appelledo_something_else(p)
, qui vérifie les pointeurs nuls, et ne fait donc rien. Aucun crash ou comportement inattendu n'est observé.Avec GCC 6, vous obtenez toujours l'appel à
p->valid()
, mais le compilateur déduit maintenant de cette expression quip
doit être non-null (sinon cep->valid()
serait un comportement non défini) et prend note de cette information. Ces informations déduites sont utilisées par l'optimiseur de sorte que si l'appel àdo_something_else(p)
est incorporé, laif (p)
vérification est maintenant considérée comme redondante, car le compilateur se souvient qu'elle n'est pas nulle, et donc intègre le code à:Cela fait maintenant vraiment déréférencer un pointeur nul, et donc le code qui semblait auparavant fonctionner cesse de fonctionner.
Dans cet exemple, le bogue est présent
func
, qui aurait dû d'abord vérifier null (ou les appelants n'auraient jamais dû l'appeler avec null):Un point important à retenir est que la plupart des optimisations comme celle-ci ne sont pas un cas où le compilateur dit "ah, le programmeur a testé ce pointeur contre null, je vais le supprimer juste pour être ennuyeux". Ce qui se passe, c'est que diverses optimisations courantes telles que l'inlining et la propagation de la plage de valeurs se combinent pour rendre ces vérifications redondantes, car elles surviennent après une vérification antérieure ou une déréférence. Si le compilateur sait qu'un pointeur est non nul au point A dans une fonction et que le pointeur n'est pas modifié avant un point B ultérieur dans la même fonction, alors il sait qu'il est également non nul en B.Lorsque l'inlining se produit les points A et B peuvent en fait être des morceaux de code qui étaient à l'origine dans des fonctions séparées, mais qui sont maintenant combinés en un seul morceau de code, et le compilateur est capable d'appliquer sa connaissance que le pointeur est non nul à plusieurs endroits.
la source
this
?this
à null] " ?this
, alors utilisez simplement-fsanitize=undefined
La norme C ++ est rompue de manière importante. Malheureusement, plutôt que de protéger les utilisateurs de ces problèmes, les développeurs de GCC ont choisi d'utiliser un comportement indéfini comme excuse pour implémenter des optimisations marginales, même lorsqu'il leur a été clairement expliqué à quel point il est dangereux.
Ici, une personne beaucoup plus intelligente que je ne l'explique en détail. (Il parle de C mais la situation est la même ici).
Pourquoi est-ce nocif?
Recompiler simplement du code sécurisé précédemment fonctionnel avec une version plus récente du compilateur peut introduire des vulnérabilités de sécurité . Bien que le nouveau comportement puisse être désactivé avec un indicateur, les fichiers makefiles existants n'ont pas cet indicateur défini, évidemment. Et comme aucun avertissement n'est produit, il n'est pas évident pour le développeur que le comportement précédemment raisonnable a changé.
Dans cet exemple, le développeur a inclus une vérification du dépassement d'entier, en utilisant
assert
, qui mettra fin au programme si une longueur non valide est fournie. L'équipe GCC a supprimé la vérification sur la base que le débordement d'entier n'est pas défini, donc la vérification peut être supprimée. Cela a abouti à la redéfinition de la vulnérabilité des instances réelles de cette base de code après la résolution du problème.Lisez le tout. C'est assez pour te faire pleurer.
OK, mais qu'en est-il de celui-ci?
Il y a longtemps, il y avait un idiome assez courant qui ressemblait à ceci:
Donc, l'idiome est: Si
pObj
n'est pas nul, vous utilisez le handle qu'il contient, sinon vous utilisez un handle par défaut. Ceci est encapsulé dans laGetHandle
fonction.L'astuce est que l'appel d'une fonction non virtuelle n'utilise en fait aucun
this
pointeur, donc il n'y a pas de violation d'accès.Je ne comprends toujours pas
Il existe beaucoup de code écrit comme ça. Si quelqu'un le recompile simplement, sans changer de ligne, chaque appel à
DoThing(NULL)
est un bug qui plante - si vous êtes chanceux.Si vous n'êtes pas chanceux, les appels aux bogues qui se bloquent deviennent des vulnérabilités d'exécution à distance.
Cela peut se produire même automatiquement. Vous avez un système de construction automatisé, non? Le mettre à niveau vers le dernier compilateur est inoffensif, non? Mais maintenant ce n'est pas le cas - pas si votre compilateur est GCC.
OK alors dis-leur!
On leur a dit. Ils le font en pleine connaissance des conséquences.
mais pourquoi?
Qui peut dire? Peut-être:
Ou peut-être autre chose. Qui peut dire?
la source