Quand l'appel d'une fonction membre sur une instance nulle entraîne-t-il un comportement indéfini?

120

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 xpour le pointeur nul. En pratique, (a)ne plante pas car le thispointeur n'est jamais utilisé.

Dans la mesure où il (b)déréférence le thispointeur ( (*this).x = 5;) et qu'il thisest 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?

GManNickG
la source
Si les deux fonctions sont statiques , comment x pourrait-il être référencé dans baz ? (x est une variable membre non statique)
legends2k
4
@ legends2k: Pretend a également xété rendu statique. :)
GManNickG
Sûrement, mais pour le cas (a), cela fonctionne de la même manière dans tous les cas, c'est-à-dire que la fonction est invoquée. Cependant, en remplaçant la valeur du pointeur de 0 à 1 (par exemple, via reinterpret_cast), il plante presque toujours. L'allocation de valeur de 0 et donc NULL, comme dans le cas a, représente-t-elle quelque chose de spécial pour le compilateur? Pourquoi se bloque-t-il toujours avec une autre valeur qui lui est allouée?
Siddharth Shankaran
5
Intéressant: Lors de la prochaine révision du C ++, il n'y aura plus du tout de déréférencement des pointeurs. Nous allons maintenant effectuer une indirection via des pointeurs. Pour en savoir plus, veuillez effectuer une indirection via ce lien: N3362
James McNellis
3
L'appel d'une fonction membre sur un pointeur nul est toujours un comportement non défini. Rien qu'en regardant votre code, je peux déjà sentir le comportement indéfini ramper lentement dans mon cou!
fredoverflow

Réponses:

113

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:

Une lvalue fait référence à un objet ou à une fonction.

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):

Si l'objet auquel se réfère la valeur l n'est pas un objet de type T et n'est pas un objet d'un type dérivé de T, ou si l'objet n'est pas initialisé, un programme qui nécessite cette conversion a un comportement indéfini.

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, si thisest 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:

Si E1a le type «pointeur vers la classe X», alors l'expression E1->E2est convertie dans la forme équivalente(*(E1)).E2;

*(E1)entraînera un comportement indéfini avec une interprétation stricte, et le .E2convertit 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):

Si une fonction membre non statique d'une classe X est appelée pour un objet qui n'est pas de type X ou d'un type dérivé de X, le comportement n'est pas défini.


Avec les fonctions statiques, l'interprétation stricte ou faible fait la différence. Strictement parlant, il n'est pas défini:

Un membre statique peut être référencé en utilisant la syntaxe d'accès aux membres de classe, auquel cas l'expression objet est évaluée.

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 E1n'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.

GManNickG
la source
5
+1. Poursuivant la pédanterie, sous la "définition faible", la fonction membre non statique n'a pas été appelée "pour un objet qui n'est pas de type X". Il a été appelé pour une lvalue qui n'est pas du tout un objet. La solution proposée ajoute donc le texte «ou si la lvalue est une lvalue vide» à la clause que vous citez.
Steve Jessop
Pourriez-vous clarifier un peu? En particulier, avec vos liens «problème fermé» et «problème actif», quels sont les numéros de problème? De plus, s'il s'agit d'un problème clos, quelle est exactement la réponse oui / non pour les fonctions statiques? J'ai l'impression de rater la dernière étape pour essayer de comprendre votre réponse.
Brooks Moses
4
Je ne pense pas que le défaut 315 du CWG soit aussi «fermé» que sa présence sur la page «problèmes clos» l'implique. Le raisonnement dit qu'il devrait être autorisé parce que " *pn'est pas une erreur quand pest 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.
James McNellis
1
@JamesMcNellis: D'après ce que j'ai compris, s'il y pavait une adresse matérielle qui déclencherait une action lors de la lecture, mais qui n'était pas déclarée volatile, 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 *pc'était le cas volatile, 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.
supercat
1
".E2 le convertit en rvalue," - Euh, non, ce n'est pas le cas
MM
30

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).

Mark Ransom
la source
1
GetSafeHwnd effectue d'abord un! This check et si true, retourne NULL. Ensuite, il commence une trame SEH et déréférence le pointeur. s'il existe une violation d'accès à la mémoire (0xc0000005), cela est intercepté et NULL est renvoyé à l'appelant :) Sinon, le HWND est renvoyé.
Петър Петров
@ ПетърПетров ça fait pas mal d'années que j'ai regardé le code pour 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!
Mark Ransom
J'énonce un exemple d'implémentation possible qui a le même effet, ce que cela fait vraiment est d'être rétro-ingénierie à l'aide d'un débogueur :)
Петър Петров
1
"ils ont des connaissances d'initiés sur le fonctionnement du compilateur!" - la cause de problèmes éternels pour des projets comme MinGW qui tentent d'autoriser g ++ à compiler du code qui appelle l'API Windows
MM
@MM Je pense que nous serions tous d'accord pour dire que c'est injuste. Et à cause de cela, je pense également qu'il existe une loi sur la compatibilité qui rend un tout petit peu illégal de le garder ainsi.
v.oddou