web-dev-qa-db-fra.com

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

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 attendons (b) à planter, car il n'y a pas de membre correspondant x pour le pointeur nul. En pratique, (a) ne plante pas car le pointeur this n'est jamais utilisé.

Car (b) déréférence le pointeur this ((*this).x = 5;) et this est null, le programme entre dans un comportement indéfini, car le déréférencement null est toujours dit être un comportement indéfini.

Est-ce que (a) entraîne un comportement indéfini? Et si les deux fonctions (et x) sont statiques?

114
GManNickG

(a) Et (b) Entraînent tous deux un comportement indéfini. C'est toujours un comportement indéfini d'appeler une fonction membre via un pointeur nul. Si la fonction est statique, elle n'est pas non plus techniquement définie, mais il y a un litige.


La première chose à comprendre est pourquoi c'est un comportement indé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" soit mentionné dans les notes du §1.9/4 et du §8.3.2/4, ce n'est jamais explicitement déclaré. (Les notes ne sont pas normatives.)

Cependant, on peut essayer de le déduire du §3.10/2:

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

Lors du déréférencement, le résultat est une valeur l. Un pointeur nul ne fait pas référence à un objet, donc 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 indiquée, alors que signifie "utiliser" la valeur l? Il suffit même de le générer ou de l'utiliser dans le sens plus formel de la conversion de lvalue en rvalue?

Quoi qu'il en soit, il ne peut certainement pas être converti en 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 de savoir si c'est un comportement indéfini de déférence mais pas d'utiliser la valeur d'un pointeur invalide (c'est-à-dire, obtenir une valeur l mais pas la convertir à une valeur r). Sinon, alors int *i = 0; *i; &(*i); est bien défini. Il s'agit d'un problème actif .

Nous avons donc une vue stricte "déréférencer un pointeur nul, obtenir un comportement non défini" et une vue faible "utiliser un pointeur nul déréférencé, obtenir un comportement non défini".

Nous examinons maintenant la question.


Oui, (a) Entraîne un comportement indéfini. En fait, si this 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:

Si E1 A le type "pointeur vers la classe X", alors l'expression E1->E2 Est convertie sous la forme équivalente (*(E1)).E2;

*(E1) entraînera un comportement indéfini avec une interprétation stricte, et .E2 le convertira en valeur r, ce qui en fera un comportement indéfini pour l'interprétation faible.

Il s'ensuit également que c'est un comportement non défini directement à partir de (§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 versus faible fait la différence. À strictement parler, elle n'est pas définie:

Un membre statique peut être référencé à l'aide de 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 n'était pas statique et nous déréférençons à nouveau un pointeur nul avec (*(E1)).E2.

Cependant, parce que 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) donne une valeur l, la fonction statique est résolue, *(E1) est supprimée et la fonction est appelée. Il n'y a pas de conversion de 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.

111
GManNickG

Évidemment indéfini signifie que ce 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 code de travail 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 va déréférencer le pointeur et provoquer UB. En pratique, si la fonction n'est pas virtuelle, le compilateur l'aura convertie en un appel de fonction simple en passant le pointeur comme premier paramètre ceci, en contournant le déréférencement et en créant une bombe à retardement pour l'appelé fonction membre. Si la fonction membre ne fait référence à aucune variable membre ou fonction virtuelle, elle peut en fait réussir sans erreur. N'oubliez pas que la réussite relève de l'univers "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 table virtuelle, et à coup sûr, vous allez obtenir UB (probablement un plantage, mais n'oubliez pas qu'il n'y a aucune garantie).

29
Mark Ransom