Une référence nulle est-elle possible?

102

Ce morceau de code est-il valide (et comportement défini)?

int &nullReference = *(int*)0;

Les deux g ++ et ++ clang compilation sans aucun avertissement, même lors de l' utilisation -Wall, -Wextra, -std=c++98, -pedantic, -Weffc++...

Bien sûr, la référence n'est pas réellement nulle, puisqu'elle n'est pas accessible (cela signifierait déréférencer un pointeur nul), mais nous pourrions vérifier si elle est nulle ou non en vérifiant son adresse:

if( & nullReference == 0 ) // null reference
peoro
la source
1
Pouvez-vous donner un cas où cela serait réellement utile? En d'autres termes, est-ce juste une question théorique?
cdhowie
Eh bien, les références sont-elles jamais indispensables? Les pointeurs peuvent toujours être utilisés à leur place. Une telle référence nulle vous permettrait également d'utiliser une référence lorsque vous ne pourriez avoir aucun objet auquel faire référence. Je ne sais pas à quel point c'est sale, mais avant d'y penser, je m'intéressais à sa légalité.
peoro
8
Je pense que c'est mal vu
Par défaut
22
"nous pourrions vérifier" - non, vous ne pouvez pas. Il existe des compilateurs qui transforment l'instruction en if (false), éliminant la vérification, précisément parce que les références ne peuvent pas être nulles de toute façon. Une version mieux documentée existait dans le noyau Linux, où une vérification NULL très similaire a été optimisée: isc.sans.edu/diary.html?storyid=6820
MSalters
2
"l'une des principales raisons d'utiliser une référence au lieu d'un pointeur est de vous libérer du fardeau d'avoir à tester pour voir si elle fait référence à un objet valide" cette réponse, dans le lien Default, sonne plutôt bien!
peoro

Réponses:

75

Les références ne sont pas des pointeurs.

8.3.2 / 1:

Une référence doit être initialisée pour faire référence à un objet ou à une fonction valide. [Note: en particulier, une référence nulle ne peut pas exister dans un programme bien défini, car le seul moyen de créer une telle référence serait de la lier à l '«objet» obtenu en déréférençant un pointeur nul, ce qui provoque un comportement indéfini. Comme décrit au 9.6, une référence ne peut pas être liée directement à un champ de bits. ]

1.9 / 4:

Certaines autres opérations sont décrites dans la présente Norme internationale comme non définies (par exemple, l'effet du déréférencement du pointeur nul)

Comme Johannes le dit dans une réponse supprimée, il y a un doute sur le fait que «déréférencer un pointeur nul» devrait être catégoriquement déclaré comme étant un comportement indéfini. Mais ce n'est pas l'un des cas qui soulèvent des doutes, puisqu'un pointeur nul ne pointe certainement pas vers un "objet ou une fonction valide", et il n'y a aucun désir au sein du comité des normes d'introduire des références nulles.

Steve Jessop
la source
J'ai supprimé ma réponse depuis que j'ai réalisé que le simple problème de déréférencer un pointeur nul et d'obtenir une lvalue qui y fait référence est une chose différente de celle de lier une référence à celui-ci, comme vous le mentionnez. Bien que les valeurs lvalues ​​se réfèrent également à des objets ou à des fonctions (donc dans ce point, il n'y a vraiment pas de différence avec une liaison de référence), ces deux choses sont toujours des préoccupations distinctes. Pour le simple fait de déréférencer, voici le lien: open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102
Johannes Schaub - litb
1
@MSalters (réponse au commentaire sur la réponse supprimée; pertinent ici) Je ne suis pas particulièrement d'accord avec la logique qui y est présentée. Bien qu'il puisse être commode d'élider &*paussi puniversellement, cela n'exclut pas un comportement indéfini (qui, de par sa nature, peut «sembler fonctionner»); et je ne suis pas d'accord qu'une typeidexpression qui cherche à déterminer le type d'un "pointeur nul déréférencé" déréférence en fait le pointeur nul. J'ai vu des gens argumenter sérieusement sur le fait que l' &a[size_of_array]on ne peut et ne devrait pas se fier, et de toute façon, il est plus facile et sûr d'écrire simplement a + size_of_array.
Karl Knechtel
Les standards @Default dans les balises [c ++] doivent être élevés. Ma réponse sonnait comme si les deux actes étaient une seule et même chose :) Bien que déréférencer et obtenir une valeur que vous ne transmettez pas et qui se réfère à "aucun objet" pourrait être faisable, le stocker dans une référence échappe à cette portée limitée et pourrait soudainement avoir un impact beaucoup plus de code.
Johannes Schaub - litb
@Karl bien en C ++, "déréférencement" ne signifie pas lire une valeur. Certaines personnes pensent que "déréférencer" signifie réellement accéder ou modifier la valeur stockée, mais ce n'est pas vrai. La logique est que C ++ dit qu'une lvalue fait référence à "un objet ou une fonction". Si tel est le cas, la question est de savoir à quoi se *préfère la valeur l , quand pest un pointeur nul. Le C ++ n'a actuellement pas la notion de lvalue vide, que le problème 232 voulait introduire.
Johannes Schaub - litb
Détection des pointeurs nuls déréférencés dans les typeidœuvres basées sur la syntaxe, au lieu de basées sur la sémantique. C'est, si vous typeid(0, *(ostream*)0)vous faites avoir un comportement non défini - bad_typeidn'est garanti d'être jeté, même si vous passez un lvalue résultant d'un déréférence de pointeur NULL sémantiquement. Mais syntaxiquement au niveau supérieur, ce n'est pas une déréférence, mais une expression d'opérateur virgule.
Johannes Schaub - litb
26

La réponse dépend de votre point de vue:


Si vous jugez par le standard C ++, vous ne pouvez pas obtenir une référence nulle car vous obtenez d'abord un comportement non défini. Après cette première incidence de comportement indéfini, la norme permet que tout se passe. Donc, si vous écrivez *(int*)0, vous avez déjà un comportement indéfini car vous êtes, d'un point de vue standard du langage, en déréférençant un pointeur nul. Le reste du programme n'est pas pertinent, une fois cette expression exécutée, vous êtes hors du jeu.


Cependant, en pratique, les références nulles peuvent facilement être créées à partir de pointeurs nulles et vous ne le remarquerez pas tant que vous n'aurez pas réellement essayé d'accéder à la valeur derrière la référence null. Votre exemple est peut-être un peu trop simple, car tout bon compilateur d'optimisation verra le comportement indéfini et optimisera simplement tout ce qui en dépend (la référence nulle ne sera même pas créée, elle sera optimisée).

Pourtant, cette optimisation dépend du compilateur pour prouver le comportement non défini, ce qui peut ne pas être possible. Considérez cette fonction simple dans un fichier converter.cpp:

int& toReference(int* pointer) {
    return *pointer;
}

Lorsque le compilateur voit cette fonction, il ne sait pas si le pointeur est un pointeur nul ou non. Il génère donc simplement du code qui transforme n'importe quel pointeur en référence correspondante. (Btw: c'est un noop puisque les pointeurs et les références sont exactement la même bête dans l'assembleur.) Maintenant, si vous avez un autre fichier user.cppavec le code

#include "converter.h"

void foo() {
    int& nullRef = toReference(nullptr);
    cout << nullRef;    //crash happens here
}

le compilateur ne sait pas que toReference()va déréférencer le pointeur passé et suppose qu'il retourne une référence valide, qui se trouvera être une référence nulle en pratique. L'appel réussit, mais lorsque vous essayez d'utiliser la référence, le programme se bloque. J'espère. La norme permet que tout se passe, y compris l'apparition d'éléphants roses.

Vous pouvez vous demander pourquoi c'est pertinent, après tout, le comportement indéfini a déjà été déclenché à l'intérieur toReference() . La réponse est le débogage: les références nulles peuvent se propager et proliférer comme le font les pointeurs nuls. Si vous n'êtes pas conscient que des références nulles peuvent exister et que vous apprenez à éviter de les créer, vous pouvez passer un certain temps à essayer de comprendre pourquoi votre fonction membre semble planter quand elle essaie simplement de lire un ancien intmembre (réponse: l'instance dans l'appel du membre était une référence nulle, de même thisqu'un pointeur nul, et votre membre est calculé pour être localisé en tant qu'adresse 8).


Alors, que diriez-vous de vérifier les références nulles? Vous avez donné la ligne

if( & nullReference == 0 ) // null reference

dans votre question. Eh bien, cela ne fonctionnera pas: selon la norme, vous avez un comportement non défini si vous déréférencer un pointeur nul, et vous ne pouvez pas créer une référence null sans déréférencer un pointeur nul, donc les références nulles n'existent que dans le domaine du comportement non défini. Puisque votre compilateur peut supposer que vous ne déclenchez pas de comportement indéfini, il peut supposer qu'il n'y a pas de référence nulle (même s'il émettra facilement du code qui génère des références nulles!). En tant que tel, il voit la if()condition, conclut que cela ne peut pas être vrai et jette simplement la if()déclaration entière . Avec l'introduction des optimisations de temps de liaison, il est devenu tout à fait impossible de vérifier les références nulles de manière robuste.


TL; DR:

Les références nulles sont en quelque sorte une existence horrible:

Leur existence semble impossible (= par le standard),
mais elles existent (= par le code machine généré),
mais vous ne pouvez pas les voir si elles existent (= vos tentatives seront optimisées),
mais elles peuvent vous tuer de toute façon inconscient (= votre programme plante à des moments bizarres, ou pire).
Votre seul espoir est qu'ils n'existent pas (= écrivez votre programme pour ne pas les créer).

J'espère que cela ne vous hantera pas!

cmaster - réintégrer monica
la source
2
Qu'est-ce qu'un "éléphant ping"?
Pharap
2
@Pharap Je n'ai aucune idée, c'était juste une faute de frappe. Mais le standard C ++ ne se soucierait pas de savoir si ce sont des éléphants roses ou ping qui apparaissent, de toute façon ;-)
cmaster - rétablir monica
9

Si votre intention était de trouver un moyen de représenter null dans une énumération d'objets singleton, alors c'est une mauvaise idée de (dé) référencer null (it C ++ 11, nullptr).

Pourquoi ne pas déclarer un objet singleton statique qui représente NULL dans la classe comme suit et ajouter un opérateur cast-to-pointer qui retourne nullptr?

Edit: Correction de plusieurs erreurs de typage et ajout d'une instruction if dans main () pour tester l'opérateur cast-to-pointer fonctionnant réellement (ce que j'ai oublié de ... mon mauvais) - 10 mars 2015 -

// Error.h
class Error {
public:
  static Error& NOT_FOUND;
  static Error& UNKNOWN;
  static Error& NONE; // singleton object that represents null

public:
  static vector<shared_ptr<Error>> _instances;
  static Error& NewInstance(const string& name, bool isNull = false);

private:
  bool _isNull;
  Error(const string& name, bool isNull = false) : _name(name), _isNull(isNull) {};
  Error() {};
  Error(const Error& src) {};
  Error& operator=(const Error& src) {};

public:
  operator Error*() { return _isNull ? nullptr : this; }
};

// Error.cpp
vector<shared_ptr<Error>> Error::_instances;
Error& Error::NewInstance(const string& name, bool isNull = false)
{
  shared_ptr<Error> pNewInst(new Error(name, isNull)).
  Error::_instances.push_back(pNewInst);
  return *pNewInst.get();
}

Error& Error::NOT_FOUND = Error::NewInstance("NOT_FOUND");
//Error& Error::NOT_FOUND = Error::NewInstance("UNKNOWN"); Edit: fixed
//Error& Error::NOT_FOUND = Error::NewInstance("NONE", true); Edit: fixed
Error& Error::UNKNOWN = Error::NewInstance("UNKNOWN");
Error& Error::NONE = Error::NewInstance("NONE");

// Main.cpp
#include "Error.h"

Error& getError() {
  return Error::UNKNOWN;
}

// Edit: To see the overload of "Error*()" in Error.h actually working
Error& getErrorNone() {
  return Error::NONE;
}

int main(void) {
  if(getError() != Error::NONE) {
    return EXIT_FAILURE;
  }

  // Edit: To see the overload of "Error*()" in Error.h actually working
  if(getErrorNone() != nullptr) {
    return EXIT_FAILURE;
  }
}
David Lee
la source
parce que c'est lent
wandalen
6

clang ++ 3.5 y met même en garde:

/tmp/a.C:3:7: warning: reference cannot be bound to dereferenced null pointer in well-defined C++ code; comparison may be assumed to
      always evaluate to false [-Wtautological-undefined-compare]
if( & nullReference == 0 ) // null reference
      ^~~~~~~~~~~~~    ~
1 warning generated.
Jan Kratochvil
la source