Une partie du code "pratique" (drôle de façon d'épeler "buggy") qui était cassé ressemblait à ceci :
void foo(X* p) {
p->bar()->baz();
}
et il a oublié de tenir compte du fait que p->bar()
renvoie parfois un pointeur nul, ce qui signifie que le déréférencer pour appeler baz()
est indéfinie.
Tout le code qui a été cassé ne contenait pas d'explicite if (this == nullptr)
ou if (!p) return;
des contrôles. Dans certains cas, il s'agissait simplement de fonctions qui n'accédaient à aucune variable membre, et ainsi est apparu pour fonctionner correctement. Par exemple :
struct DummyImpl {
bool valid() const { return false; }
int m_data;
};
struct RealImpl {
bool valid() const { return m_valid; }
bool m_valid;
int m_data;
};
template<typename T>
void do_something_else(T* p) {
if (p) {
use(p->m_data);
}
}
template<typename T>
void func(T* p) {
if (p->valid())
do_something(p);
else
do_something_else(p);
}
Dans ce code, lorsque vous appelez func<DummyImpl*>(DummyImpl*)
avec un pointeur nul, il y a un déréférencement "conceptuel" du pointeur pour appeler p->DummyImpl::valid()
mais en fait, cette fonction membre renvoie juste false
sans accéder à *this
. Ce return false
peut être inlined et donc en pratique le pointeur n'a pas besoin d'être accédé du tout. Ainsi, avec certains compilateurs, cela semble fonctionner correctement : il n'y a pas de segfault pour le déréférencement de null, p->valid()
est faux, donc le code appelle do_something_else(p)
qui vérifie les pointeurs nuls, et ne fait donc rien. Aucun plantage 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 que p
doit être non nulle (sinon p->valid()
serait un comportement indéfini) et prend note de cette information. Cette information déduite est utilisée par l'optimiseur de sorte que si l'appel à do_something_else(p)
est inlined, le if (p)
est maintenant considéré comme redondant, car le compilateur se souvient qu'il n'est pas nul, et met donc en ligne le code pour :
template<typename T>
void func(T* p) {
if (p->valid())
do_something(p);
else {
// inlined body of do_something_else(p) with value propagation
// optimization performed to remove null check.
use(p->m_data);
}
}
Cela déréférence réellement un pointeur nul, et donc le code qui semblait fonctionner auparavant cesse de fonctionner.
Dans cet exemple, le bogue se trouve dans func
qui aurait dû vérifier l'absence de null en premier lieu (ou les appelants n'auraient jamais dû l'appeler avec null) :
template<typename T>
void func(T* p) {
if (p && p->valid())
do_something(p);
else
do_something_else(p);
}
Il est important de se rappeler que la plupart des optimisations de ce type 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 comme l'inlining et la propagation de la plage de valeurs se combinent pour rendre ces vérifications redondantes, parce qu'elles viennent 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 d'une fonction, et que le pointeur n'est pas modifié avant un point B ultérieur de la même fonction, alors il sait qu'il est également non nul au point 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 à plus d'endroits. Il s'agit d'une optimisation de base, mais très importante, et si les compilateurs ne faisaient pas cela, le code de tous les jours serait considérablement plus lent et les gens se plaindraient de branches inutiles pour tester à nouveau les mêmes conditions à plusieurs reprises.
2 votes
stackoverflow.com/a/1844012/1870760 est une bonne lecture. Si votre compilateur vous le permet, vous pouvez faire des hypothèses à partir de cela.
0 votes
Oui, certains développeurs ont un code qui suppose que l'objet peut être un pointeur nul. Par exemple, une bibliothèque peut systématiquement définir les objets comme nullptr après les avoir supprimés, puis appeler certaines fonctions de ces objets supprimés.
0 votes
Duplicata possible de Vérification de la nullité de l'objet
1 votes
Ce n'est pas non plus le seul exemple de CCG faisant de telles choses. gcc.gnu.org/bugzilla/show_bug.cgi?id=30475
1 votes
Cet article fournit une bonne explication de la raison pour laquelle de telles bases de code sont cassées. La base de code de Qt 5 est malheureusement un peu cassée à cet égard, mais nous espérons que cela sera corrigé en temps voulu.
13 votes
Rappelez-vous que cela n'affecte pas seulement le code qui a
if(this == 0) { ... }
mais aussi du code qui passethis
au code qui faitif(ptr == 0)
par inlining. J'ai rencontré ce problème dans Qt lorsque j'ai appeléobj->deleteLater()
sur un pointeur nul, qui appelle QCoreApplication::postEvent en lui passant le nullthis
. Si cette fonction était inlined (ce qui est peu probable pourpostEvent
), un avertissement peut se transformer en collision. Les programmeurs négligents peuvent penserptr->deleteLater()
se comporte commedelete ptr;
à cet égard et tomber dans le piège.21 votes
@Ben J'espère que vous le pensez dans le bon sens. Le code avec UB doit être réécrit pour ne pas invoquer UB. C'est aussi simple que cela. D'ailleurs, il existe souvent des FAQ qui vous expliquent comment y parvenir. Il ne s'agit donc pas d'un véritable problème, à mon avis. Tout va bien.
3 votes
@KubaOber, Non : J'accuse. La base de code existante est trop importante pour que "réparer votre code" soit la solution. La seule façon d'avancer est de définir le comportement indéfini pour réifier les hypothèses faites dans le code existant.
4 votes
@KubaOber, en d'autres termes, la réponse doit être de retirer UB de la norme, et de considérer la norme comme cassée jusqu'à ce que cela soit fait. Il est clair qu'une norme cassée doit être réparée, et non mise en œuvre telle quelle ! .... Utiliser les parties cassées pour justifier le sabotage du code existant n'est pas quelque chose que quiconque devrait être autorisé à faire. C'est si manifestement faux que cela doit être délibéré.
4 votes
Veuillez clarifier votre question. Le titre demande pourquoi GCC veut casser du code C++ pratique, et la question en gras dans le corps de la question demande pourquoi les projets feraient
this == null
( ?). Ce sont des questions distinctes.50 votes
Je suis étonné de voir des gens défendre le déréférencement des pointeurs nuls dans le code. Tout simplement étonnant.
5 votes
@sergeyA Personne ne fait ça. Nous disons que le compilateur ne devrait pas supprimer une vérification de nullité juste parce que le pointeur est
this
. La vérification de null est là pour éviter le déréférencement de null. L'excuse est "this
ne peut être nulle", mais bien sûr en réalité, il peut . Voir aussi gcc.gnu.org/bugzilla/show_bug.cgi?id=30475 où GCC supprime la vérification du dépassement des nombres entiers - l'excuse ? Le dépassement des nombres entiers n'est pas défini, il n'est donc pas nécessaire de le vérifier. Et pourtant, cela arrive alors vous faire Il faut le vérifier.19 votes
@Ben, l'explosion d'un comportement indéfini est une tactique d'optimisation très efficace depuis très longtemps. Je l'adore, car j'aime les optimisations qui permettent à mon code de s'exécuter plus rapidement.
17 votes
Je suis d'accord avec SergeyA. Tout le brouhaha a commencé parce que les gens semblent s'attarder sur le fait que
this
est passé comme un paramètre implicite, ils commencent alors à l'utiliser comme si c'était un paramètre explicite. Ce n'est pas le cas. Lorsque vous déréférencez un null this, vous invoquez UB comme si vous déréférenciez n'importe quel autre pointeur null. C'est tout ce qu'il y a à faire. Si vous voulez faire passer des nullptrs, utiliser un paramètre explicite, DUH . Il ne sera pas plus lent, il ne sera pas plus lourd, et le code qui possède une telle API est de toute façon profondément ancré dans les internes, et a donc une portée très limitée. Fin de l'histoire, je pense.15 votes
@Ben Il existe une manière parfaitement valide d'écrire des contrôles de dépassement d'entier ( FAQ ), il se trouve que les personnes dont le code a été "cassé" par les améliorations de gcc ne connaissaient pas leur C au départ. Il y a une quantité absurde de mauvais C/C++. Cela ne signifie pas que les compilateurs doivent se plier en quatre pour le supporter. Un tel code devrait être éradiqué, et les compilateurs devraient nous pousser vers ce but...
3 votes
@JohannesSchaub-litb Appeler une méthode sur un pointeur nul n'a pas de sens, étant donné que
foo->deleteLater()
quandfoo
est nulle est déjà UB, avant que nous n'ayons à effectuer les vérifications qui pourraient se produire à l'intérieur de la fonctiondeleteLater
. Un appel de méthode non statique ne peut pas être un remplacement direct d'une expression de suppression ! Si vous le souhaitez, vous pouvez implémenter un appel de méthode non statique.void deleteLater(QObject * o) { if (o) o->deleteLater(); }
. C'est valable et, à mon avis, la seule solution raisonnable au problème que vous décrivez.6 votes
"Pourquoi cette nouvelle hypothèse casserait-elle le code C++ pratique ?" - Ce n'est pas une réponse à votre question spécifique, mais ce billet de blog d'un développeur LLVM/Clang donne un bon aperçu de la façon dont les compilateurs optimisateurs exploitent le comportement indéfini pour obtenir des gains de performance (importants), et comment cela peut conduire à de mauvaises surprises, même pour les programmeurs expérimentés.
41 votes
Félicitations à GCC pour avoir brisé le cycle du mauvais code -> compilateur inefficace pour supporter le mauvais code -> plus de mauvais code -> plus de compilation inefficace -> ...
0 votes
Cela devrait également signifier que
typeid(*this)
devient effectivement "nothrow" dans GCC. Jusqu'à présent, il a pu lancer bad_typeid, je pense9 votes
Il a été noté que
this
est un pointeur seulement parce que les références n'existaient pas quandthis
a été introduit en C++. GCC sait déjà que les références ne peuvent pas êtrenull
; c'est juste GCC qui est cohérent.1 votes
@KubaOber Oui, je souhaite le meilleur à Qt pour assainir sa base de code. Ils ont fait un peu mieux ces derniers temps en se modernisant avec les fonctionnalités de C++11 et 14 - si seulement leurs tutoriels n'étaient pas terriblement dépassés, par exemple en indiquant toutes les fonctionnalités de C++11 et 14.
SIGNALS/SLOTS
les macro-déchets sont toujours nécessaires, alors qu'ils ne le sont pas. Des choix stylistiques que je n'aime pas - et des UB que je ne mettrai pas près de mes programmes - m'empêchent de l'essayer. Je m'en tiendrai àgtkmm
Merci ! Pas de macros et adoption beaucoup plus enthousiaste de C++11/14.0 votes
@KubaOber "L'appel d'une méthode sur un pointeur nul n'a aucun sens, puisque
foo->deleteLater()
quandfoo
est nul est déjà UB" D'après ce que j'ai compris LWG 315 à un moment donné, c'était pas devait être UB, mais la formulation ne semble pas avoir changé (elle est toujours UB selon [class.mfct.non-static]).4 votes
@dyp Il n'est généralement pas possible de faire
foo->method()
ne pas être UB sifoo
est nulle.method()
peut être virtuelle, ou peut provenir d'une classe de base et alors quefoo
est d'une classe dérivée etmethod()
recevra un montant ajusté dethis
qui a une valeur d'un petit négatifintptr_t
et ainsi de suite. Cela n'a aucun sens d'essayer de faire en sorte qu'il n'y ait pas d'UB dans certains cas particuliers. Quelles que soient les intentions, elles ont été sainement abandonnées.0 votes
@KubaOber D'oh j'ai encore mal lu le CWG 315 (et l'ai appelé un défaut de LWG).... Il s'agit d'appeler statique les fonctions membres, et non non statique les fonctions des membres. Désolé pour la confusion.
2 votes
@Ben : Si vous arrivez à un point où
this
est nulle, il est trop tard : vous avez déjà fait quelque chose de mal - dans les exemples donnés, c'est parce que vous avez déjà déréférencé un pointeur nul, et pour une raison quelconque, le compilateur a décidé de transformer cela en un appel de méthode avec un pointeur nul.this
plutôt que d'avoir un autre effet.2 votes
@Hurkyl, C++ a été standardisé en 1998. Une grande partie de ce code a été écrite avant cela, et devait cibler le compilateur et non le standard - parce qu'il n'y avait pas de norme à l'époque . Après 1998, il a fallu des années avant que TOUT compilateur n'implémente la norme. Une grande partie de ce code fonctionne encore aujourd'hui avec des mises à jour mineures, juste des portages sur de nouvelles architectures.
2 votes
Par exemple, Qt date de 1991. Chrome est issu de KHTML qui a été publié en 1997. Ces idiomes sont intégrés dans une quantité énorme de logiciels et ne sont pas prêts de disparaître. L'échec de la norme à accepter le code C++ existant et fonctionnel comme valide est la raison pour laquelle la norme est cassée.
4 votes
Redites-moi donc comment tout programmeur qui invoque UB est un incompétent, un indigne, un pourri, et mérite d'être écrasé par un train. Expliquez à nouveau - pour mon patron cette fois - comment nous devons passer des mois et 10Ks de dollars pour réécrire 200klocs de C++ fonctionnel ou nous méritons d'être frappés au visage de façon répétée par l'équipe GCC. BATTEZ-MOI ENCORE UNE FOIS ! JE VEUX ÊTRE PUR ! ........ Ou, nous pourrions... simplement ne pas utiliser un compilateur écrit par des sadiques pour des masochistes.
9 votes
@Ben : C'est exact -- vous pouvez continuer à utiliser des compilateurs anciens (ou même des compilateurs modernes avec les bons drapeaux !) pour compiler votre ancien code non conforme. Ainsi, vous n'offrez aucune bonne raison pour laquelle les choses devraient être ruinées pour ceux d'entre nous qui écrivent du code conforme aux normes.
0 votes
@Ben : Savez-vous s'il a déjà été question de modifier la norme pour faire du déréférencement d'un pointeur nul pour appeler une fonction membre non virtuelle et non statique une chose bien définie ?
3 votes
@Hurkyl, je n'en ai aucune idée. D'autres langues le font pourtant. Et vous êtes le bienvenu dans votre relation dysfonctionnelle avec gcc (il ne vous frappe que parce qu'il vous aime, continuez à vous le dire) mais j'ai choisi d'utiliser des compilateurs qui ne changent pas les règles et ne m'en font pas porter la responsabilité. ajouter UB ? C'est pourquoi je n'écris pas de nouveau code en C++), mais je dois quand même maintenir les vieux trucs. Pendant ce temps, les gens passent à CLang parce que "les bugs de plantage mystérieux disparaissent". CLang ne vous oblige pas à chercher le nouveau bouton "Don't randomly crash" à chaque version. Y en a-t-il 5 maintenant pour GCC ?
0 votes
@Ben : Je n'arrête pas de lire que l'ajout de nouvelles formes d'UB est censé "améliorer les performances", mais le langage utilisé dans les années 1990, qui interprétait les règles de type pointeur comme étant applicables uniquement aux variables nommées, et excluait les tableaux qui n'étaient jamais accédés par leur nom (mais créés uniquement pour être utilisés comme pools de mémoire) permettait d'opérer sur des données avec des morceaux de deux et quatre octets sans tenir compte du type de données en question, sans que le compilateur ait à supposer que chaque accès de ce type pourrait toucher chaque objet nommé dont l'adresse a été exposée au code extérieur. En disant que le code qui veut...
0 votes
...pour utiliser un accès agnostique, il faut soit utiliser des types de caractères, soit utiliser memcpy, qui sont tous deux carrément horribles du point de vue de l'aliasing, ce qui ne me semble pas être un gain de performance.
4 votes
@Ben "Par exemple Qt date de 1991. Chrome est issu de KHTML qui a été publié en 1997. Ces idiomes sont intégrés dans une énorme quantité de logiciels et ne sont pas prêts de disparaître." - sauf qu'ils tout à fait si les codeurs corrigent leurs bases de code pour éviter l'ub. si vous ne voulez pas, alors peu importe, mais ne dépeignez pas cela comme un fait inévitable de la vie quand c'est dû purement à l'inertie ou au refus actif de suivre la norme.
0 votes
@underscore_d Vous n'avez rien compris. La norme n'avait pas besoin d'être comme ça : ce comportement aurait pu être défini, ou rendu défini par l'implémentation. C'était une décision, et c'était une mauvaise décision. La norme est mauvaise et doit être modifiée.
0 votes
Bien que je ne puisse pas vraiment dire grand chose ici, puisque je n'étais qu'un petit enfant avant la standardisation du C++ et que je ne savais même pas que le langage existait, je pense que des changements comme celui-ci devraient initialement être implémentés comme opt-in au lieu d'opt-out, et dans le cas d'optimisations qui font des suppositions comme celle-ci, un utilitaire autonome devrait être fourni qui peut être utilisé pour analyser le code et émettre des avertissements lorsqu'il rencontre une situation qui pourrait causer des problèmes si l'optimisation était activée. Cela permet aux gens de savoir ce qui doit être modifié. avant il les visse, et indique le
0 votes
La probabilité que cela leur cause des problèmes s'ils ne modifient pas leur code. Les versions ultérieures passeraient alors de l'opt-in à l'opt-out, après avoir donné aux gens environ 6 à 12 mois pour s'y habituer (et pour laisser suffisamment de temps aux anciennes bibliothèques pour être réécrites). Ils pourraient également fournir un pragma qui peut être utilisé pour désactiver cette optimisation particulière pour un bloc de code, de sorte que les bibliothèques puissent indiquer quelle(s) partie(s) de leur base de code est/sont encore susceptible(s) de poser des problèmes ; idéalement, ce pragma générerait automatiquement un avertissement.
1 votes
Aussi, ce fonctionne sur Clang, GCC, et MSVC. Je voulais juste mettre cela sur la table.
1 votes
@JustinTime : La bonne approche devrait être de définir des directives qui indiqueraient qu'un programme ne s'appuie pas sur certaines constructions, et suggérer que le code de qualité devrait utiliser cette directive chaque fois que possible. Javascript a adopté cette approche avec son dialecte "strict" : si un programme commence par la chaîne de caractères "use strict", il sera traité avec des règles de portée plus strictes qui sont plus sûres et permettent plus d'optimisations que ce qui serait possible autrement, mais qui seraient incompatibles avec certains codes existants. Plutôt que d'essayer d'argumenter pour savoir si un morceau de code doit être supporté, ...
1 votes
...les programmeurs et les auteurs de compilateurs auraient dû convenir que si le code inclut une directive disant "J'ai besoin de cette sémantique", il serait stupide d'optimiser en supposant que le code n'en a pas besoin, tout en convenant également que le code de qualité devrait inclure une directive indiquant si cette sémantique est requise. Si le code n'inclut aucune directive dans un sens ou dans l'autre, les auteurs de compilateurs pourraient utiliser leur jugement du public visé pour choisir un comportement par défaut.
0 votes
D'accord, @supercat. Si l'on considère le nombre de vieilles bases de code qui utilisent des techniques et des pratiques qui sont devenues par la suite des BPU, ce serait probablement très Il serait utile d'avoir un moyen de dire de manière programmatique au compilateur "nous sommes bons, optimisons" ou "nous ne sommes pas encore prêts", à partir du code source lui-même. ...Cela aurait également l'avantage de rappeler à toute personne qui ouvre le fichier que le code est périmé et qu'il doit être remanié et/ou nettoyé, ce qui est un plus.
1 votes
@JustinTime : Notez que l'UB était jamais destiné à encourager les démons nasaux. Si l'on regarde le raisonnement, l'intention était que des implémentations de qualité pour des cibles et des objectifs divers définissent des comportements appropriés pour ces cibles et ces objectifs. Les auteurs de la norme reconnaissent ouvertement qu'il serait possible qu'une implémentation "conforme" soit d'une qualité si médiocre qu'elle en deviendrait inutile, et le fait que la norme permette à une implémentation "conforme" de se conformer à la norme est une bonne chose. conforme compilateur à faire quelque chose n'implique en aucun cas qu'une telle action ne rendrait pas un compilateur inadapté à certains objectifs.
1 votes
@JustinTime : Il serait amusant de donner aux défenseurs de la folie du compilateur un code simple et de leur demander de le réécrire d'une manière strictement conforme à la façon dont 6.5p7 est écrit (en évitant les constructions que les implémentations non-garbage évitent évidemment). devrait traiter comme défini, mais que la norme ne permet pas réellement). Selon une lecture hyper-pédagogique, étant donné que
int i=0;
ou même quelque chose commei=1;
puisque la valeur li
en soi ne modifie pasi
et l'expression d'affectation n'est pas une lvalue d'un type approprié parce qu'elle n'est pas une lvalue. Ce n'est un problème que pour les compilateurs obtus, bien sûr...1 votes
...mais je dirais la même chose de beaucoup de constructions qui sont brisées par l'"optimisation".