Considérez le code suivant:
#include <iostream>
struct foo
{
// (a):
void bar() { std::cout << "gman was here" << std::endl; }
// (b):
void baz() { x = 5; }
int x;
};
int main()
{
foo* f = 0;
f->bar(); // (a)
f->baz(); // (b)
}
Nous prévoyons (b)
de planter, car il n'y a pas de membre correspondant x
pour le pointeur nul. En pratique, (a)
ne plante pas car le this
pointeur n'est jamais utilisé.
Dans la mesure où il (b)
déréférence le this
pointeur ( (*this).x = 5;
) et qu'il this
est nul, le programme entre un comportement indéfini, car le déréférencement nul est toujours considéré comme un comportement indéfini.
Cela (a)
entraîne- t-il un comportement indéfini? Et si les deux fonctions (et x
) sont statiques?
x
été rendu statique. :)Réponses:
Les deux
(a)
et(b)
entraînent un comportement indéfini. L'appel d'une fonction membre via un pointeur nul est toujours un comportement indéfini. Si la fonction est statique, elle est techniquement indéfinie également, mais il y a un différend.La première chose à comprendre est pourquoi il est un comportement non défini de déréférencer un pointeur nul. En C ++ 03, il y a en fait un peu d'ambiguïté ici.
Bien que "déréférencer un pointeur nul entraîne un comportement indéfini" est mentionné dans les notes du §1.9 / 4 et du §8.3.2 / 4, il n'est jamais explicitement indiqué. (Les notes ne sont pas normatives.)
Cependant, on peut essayer de le déduire du §3.10 / 2:
Lors du déréférencement, le résultat est une lvalue. Un pointeur nul ne fait pas référence à un objet, par conséquent, lorsque nous utilisons la valeur l, nous avons un comportement indéfini. Le problème est que la phrase précédente n'est jamais énoncée, alors que signifie "utiliser" la lvalue? Le générer même pas du tout, ou l'utiliser dans le sens plus formel de la conversion d'une valeur à une valeur?
Quoi qu'il en soit, il ne peut certainement pas être converti en une valeur r (§4.1 / 1):
Ici, c'est définitivement un comportement indéfini.
L'ambiguïté vient du fait qu'il s'agit ou non d'un comportement indéfini de déférence mais pas d'utiliser la valeur d'un pointeur invalide (c'est-à-dire obtenir une lvalue mais pas la convertir en une rvalue). Sinon, alors
int *i = 0; *i; &(*i);
est bien défini. C'est un problème actif .Nous avons donc une vue stricte «déréférencer un pointeur nul, obtenir un comportement indéfini» et une vue faible «utiliser un pointeur nul déréférencé, obtenir un comportement indéfini».
Maintenant, nous considérons la question.
Oui,
(a)
entraîne un comportement indéfini. En fait, sithis
est nul, alors quel que soit le contenu de la fonction, le résultat n'est pas défini.Cela découle du §5.2.5 / 3:
*(E1)
entraînera un comportement indéfini avec une interprétation stricte, et le.E2
convertit en une rvalue, ce qui en fait un comportement indéfini pour l'interprétation faible.Il s'ensuit également que c'est un comportement indéfini directement à partir du (§9.3.1 / 1):
Avec les fonctions statiques, l'interprétation stricte ou faible fait la différence. Strictement parlant, il n'est pas défini:
Autrement dit, il est évalué comme s'il était non statique et nous déréférencerons à nouveau un pointeur nul avec
(*(E1)).E2
.Cependant, comme il
E1
n'est pas utilisé dans un appel de fonction membre statique, si nous utilisons l'interprétation faible, l'appel est bien défini.*(E1)
entraîne une valeur l, la fonction statique est résolue,*(E1)
est rejetée et la fonction est appelée. Il n'y a pas de conversion lvalue-en-rvalue, donc il n'y a pas de comportement indéfini.En C ++ 0x, à partir de n3126, l'ambiguïté demeure. Pour l'instant, soyez prudent: utilisez l'interprétation stricte.
la source
*p
n'est pas une erreur quandp
est nul à moins que la lvalue ne soit convertie en une rvalue." Cependant, cela repose sur le concept d'une "valeur vide", qui fait partie de la résolution proposée pour le défaut 232 du CWG , mais qui n'a pas été adoptée. Ainsi, avec le langage à la fois en C ++ 03 et C ++ 0x, le déréférencement du pointeur nul n'est toujours pas défini, même s'il n'y a pas de conversion lvalue-to-rvalue.p
avait une adresse matérielle qui déclencherait une action lors de la lecture, mais qui n'était pas déclaréevolatile
, l'instruction*p;
ne serait pas requise, mais serait autorisée , à lire réellement cette adresse; la déclaration&(*p);
, cependant, serait interdit de le faire. Si*p
c'était le casvolatile
, la lecture serait requise. Dans les deux cas, si le pointeur n'est pas valide, je ne vois pas comment la première instruction ne serait pas un comportement indéfini, mais je ne vois pas non plus pourquoi la deuxième instruction le serait.De toute évidence, indéfini signifie qu'il n'est pas défini , mais parfois cela peut être prévisible. Les informations que je suis sur le point de fournir ne doivent jamais être utilisées pour le fonctionnement du code car elles ne sont certainement pas garanties, mais elles peuvent s'avérer utiles lors du débogage.
Vous pourriez penser que l'appel d'une fonction sur un pointeur d'objet déréférencera le pointeur et provoquera UB. En pratique, si la fonction n'est pas virtuelle, le compilateur l'aura convertie en un simple appel de fonction en passant le pointeur comme premier paramètre this , en contournant le déréférencement et en créant une bombe à retardement pour la fonction membre appelée. Si la fonction membre ne fait référence à aucune variable membre ou fonction virtuelle, elle peut effectivement réussir sans erreur. N'oubliez pas que réussir s'inscrit dans l'univers du "indéfini"!
La fonction MFC de Microsoft GetSafeHwnd repose en fait sur ce comportement. Je ne sais pas ce qu'ils fumaient.
Si vous appelez une fonction virtuelle, le pointeur doit être déréférencé pour accéder à la vtable, et vous allez certainement avoir UB (probablement un plantage, mais rappelez-vous qu'il n'y a aucune garantie).
la source
GetSafeHwnd
, il est possible qu'ils l'aient amélioré depuis. Et n'oubliez pas qu'ils ont des connaissances d'initiés sur le fonctionnement du compilateur!