web-dev-qa-db-fra.com

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

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

int &nullReference = *(int*)0;

G ++ et clang ++ le compilent sans aucun avertissement, même avec -Wall, -Wextra, -std=c++98, -pedantic, -Weffc++...

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

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

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. [Remarque: en particulier, une référence null 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érencant un pointeur null, ce qui entraîne un comportement indéfini. Comme décrit en 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 indéfinies (par exemple, l’effet du déréférencement du pointeur nul).

Comme Johannes le dit dans une réponse supprimée, il existe un doute sur le fait de "déréférencer un pointeur nul" comme un comportement non défini. Mais ce n'est pas l'un des cas qui soulève des doutes, puisqu'un pointeur nul ne pointe certainement pas vers un "objet ou une fonction valide", et le comité des normes ne souhaite pas introduire de références nulles.

68
Steve Jessop

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


Si vous jugez par la norme C++, vous ne pouvez pas obtenir une référence null car vous obtenez d'abord un comportement indéfini. Après cette première incidence de comportement indéfini, la norme autorise tout. Donc, si vous écrivez *(int*)0, vous avez déjà un comportement indéfini, du point de vue de la norme de langage, déréférencant un pointeur nul. Le reste du programme est sans importance, une fois que cette expression est exécutée, vous êtes hors du jeu.


Toutefois, dans la pratique, il est facile de créer des références null à partir de pointeurs nuls. Vous ne le remarquerez pas tant que vous n'essayez pas d'accéder à la valeur située derrière la référence null. Votre exemple est peut-être un peu trop simple, car tout bon compilateur optimisateur verra le comportement indéfini, et optimisera simplement tout ce qui en dépend (la référence null ne sera même pas créée, elle sera optimisée).

Cependant, cette optimisation dépend du compilateur pour prouver le comportement indéfini, ce qui peut ne pas être possible. Considérons 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. Donc, il génère juste du code qui transforme n'importe quel pointeur en la référence correspondante. (Btw: Ceci est un noop car 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.cpp Avec le code

#include "converter.h"

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

le compilateur ne sait pas que toReference() déréférencera le pointeur passé et supposera qu'il renvoie une référence valide, ce qui sera en réalité une référence nulle. L'appel aboutit, mais lorsque vous essayez d'utiliser la référence, le programme se bloque. J'espère. La norme permet tout, y compris l'apparition d'éléphants ping.

Vous vous demandez peut-être pourquoi c'est pertinent, après tout, le comportement indéfini a déjà été déclenché dans 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 ne savez pas que des références nulles peuvent exister et que vous apprenez à éviter de les créer, vous risquez de passer un certain temps à essayer de comprendre pourquoi votre fonction membre semble se bloquer, alors que vous essayez simplement de lire un ancien vieux int membre (réponse: l'instance dans l'appel du membre était une référence null, donc this est un pointeur nul, et votre membre est censé se situer à l'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 indéfini si vous déréférenciez un pointeur null, et vous ne pouvez pas créer de référence null sans déréférencer un pointeur nul; Puisque votre compilateur peut supposer que vous ne déclenchez pas un comportement indéfini, il peut supposer qu’il n’existe pas de référence nulle (même s’il émettra facilement du code générant des références nulles!). En tant que tel, il voit la condition if(), conclut que cela ne peut pas être vrai et élimine simplement l'instruction entière if(). Avec l'introduction des optimisations de temps de liaison, il est devenu 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 épouvantable:

Leur existence semble impossible (= par la norme),
mais ils existent (= par le code machine généré),
mais vous ne pouvez pas les voir s’ils existent (= vos tentatives seront optimisées),
mais ils peuvent vous tuer de toute façon sans le savoir (= 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 viendra pas vous hanter!

13
cmaster

Si votre intention était de trouver un moyen de représenter null dans une énumération d'objets singleton, il est alors déconseillé de 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-pointeur qui retourne nullptr?

Edit: Correction de plusieurs types de fautes et ajout de if-statement dans main () pour tester le fonctionnement réel de l'opérateur de conversion vers le pointeur (ce que j'avais oublié de faire .. 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;
  }
}
8
David Lee

clang ++ 3.5 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.
6
Jan Kratochvil