È possibile un riferimento nullo?


102

Questo pezzo di codice è valido (e ha un comportamento definito)?

int &nullReference = *(int*)0;

Sia g ++ e clang ++ compilarlo senza alcun preavviso, anche quando si utilizza -Wall, -Wextra, -std=c++98, -pedantic, -Weffc++...

Ovviamente il riferimento non è effettivamente nullo, poiché non è possibile accedervi (significherebbe dereferenziare un puntatore nullo), ma potremmo verificare se è nullo o meno controllando il suo indirizzo:

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

1
Puoi dare qualche caso in cui questo sarebbe effettivamente utile? In altre parole, è solo una domanda teorica?
cdhowie

Ebbene, le referenze sono mai indispensabili? È sempre possibile utilizzare i puntatori al loro posto. Un tale riferimento nullo ti consentirebbe di utilizzare un riferimento anche quando non potresti avere alcun oggetto a cui fare riferimento. Non so quanto sia sporco, ma prima di pensarci mi interessava la sua legalità.
peoro

8
Penso che sia disapprovato
Predefinito

22
"potremmo controllare" - no, non puoi. Ci sono compilatori che trasformano l'istruzione in if (false), eliminando il controllo, proprio perché i riferimenti non possono comunque essere nulli. Una versione meglio documentata esisteva nel kernel Linux, dove un controllo NULL molto simile è stato ottimizzato: isc.sans.edu/diary.html?storyid=6820
MSalters

2
"uno dei motivi principali per utilizzare un riferimento invece di un puntatore è liberarti dal peso di dover testare per vedere se si riferisce a un oggetto valido" questa risposta, nel link di Default, suona abbastanza bene!
peoro

Risposte:


75

I riferimenti non sono puntatori.

8.3.2 / 1:

Un riferimento deve essere inizializzato per fare riferimento a un oggetto o una funzione valida. [Nota: in particolare, un riferimento nullo non può esistere in un programma ben definito, perché l'unico modo per creare un tale riferimento sarebbe legarlo all '"oggetto" ottenuto dereferenziando un puntatore nullo, che causa un comportamento indefinito. Come descritto in 9.6, un riferimento non può essere associato direttamente a un campo di bit. ]

1.9 / 4:

Alcune altre operazioni sono descritte in questo standard internazionale come non definite (ad esempio, l'effetto di dereferenziare il puntatore nullo)

Come dice Johannes in una risposta cancellata, c'è qualche dubbio se "dereferenziare un puntatore nullo" debba essere categoricamente dichiarato come comportamento indefinito. Ma questo non è uno dei casi che sollevano dubbi, poiché un puntatore nullo non punta certamente a un "oggetto o funzione valido", e non vi è alcun desiderio all'interno del comitato degli standard di introdurre riferimenti nulli.


Ho rimosso la mia risposta poiché mi sono reso conto che il semplice problema di dereferenziare un puntatore nullo e ottenere un lvalue che si riferisce a quello è una cosa diversa rispetto a legare effettivamente un riferimento ad esso, come dici tu. Sebbene si dica che i valori si riferiscano anche a oggetti o funzioni (quindi in questo punto, non c'è davvero differenza rispetto a un legame di riferimento), queste due cose sono ancora preoccupazioni separate. Per il semplice atto di dereferenziazione, ecco il link: open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102
Johannes Schaub - litb

1
@MSalters (risposta al commento su risposta eliminata; rilevante qui) Non posso essere particolarmente d'accordo con la logica qui presentata. Sebbene possa essere conveniente elidere &*pcome puniversalmente, ciò non esclude un comportamento indefinito (che per sua natura può "sembrare funzionare"); e non sono d'accordo che typeidun'espressione che cerca di determinare il tipo di un "puntatore nullo dereferenziato" dereferisce effettivamente il puntatore nullo. Ho visto persone discutere seriamente che &a[size_of_array]non si può e non si dovrebbe fare affidamento, e comunque è più facile e sicuro scrivere a + size_of_array.
Karl Knechtel

@ Gli standard predefiniti nei tag [c ++] dovrebbero essere alti. La mia risposta sembrava che entrambi gli atti fossero la stessa cosa :) Mentre dereferenziare e ottenere un valore che non si passa in giro che si riferisce a "nessun oggetto" potrebbe essere fattibile, memorizzarlo in un riferimento sfugge a quella portata limitata e improvvisamente potrebbe avere un impatto molto più codice.
Johannes Schaub - litb

@Karl bene in C ++, "dereferencing" non significa leggere un valore. Alcune persone pensano che "dereferenziazione" significhi effettivamente accedere o modificare il valore memorizzato, ma non è vero. La logica è che C ++ dice che un lvalue si riferisce a "un oggetto o una funzione". Se è così, allora la domanda è a cosa si *priferisce lvalue , quando pè un puntatore nullo. Il C ++ attualmente non ha la nozione di un lvalue vuoto, che il problema 232 voleva introdurre.
Johannes Schaub - litb

Rilevamento di puntatori nulli dereferenziati in typeidopere basate sulla sintassi, invece che sulla semantica. Cioè, se si fa typeid(0, *(ostream*)0)si fa avere un comportamento indefinito - non bad_typeidè garantito per essere gettato, anche se si passa un valore assegnabile risultante da un puntatore nullo dereferenziamento semanticamente. Ma sintatticamente al livello superiore, non è una dereferenziazione, ma un'espressione di operatore virgola.
Johannes Schaub - litb

26

La risposta dipende dal tuo punto di vista:


Se giudichi in base allo standard C ++, non puoi ottenere un riferimento nullo perché ottieni prima un comportamento indefinito. Dopo quella prima incidenza di comportamenti indefiniti, lo standard permette che tutto accada. Quindi, se scrivi *(int*)0, hai già un comportamento indefinito come sei, da un punto di vista standard del linguaggio, dereferenziando un puntatore nullo. Il resto del programma è irrilevante, una volta eseguita questa espressione, sei fuori dal gioco.


Tuttavia, in pratica, i riferimenti null possono essere facilmente creati da puntatori null e non te ne accorgerai finché non proverai effettivamente ad accedere al valore dietro il riferimento null. Il tuo esempio potrebbe essere un po 'troppo semplice, poiché qualsiasi buon compilatore di ottimizzazione vedrà il comportamento indefinito e ottimizzerà semplicemente tutto ciò che dipende da esso (il riferimento null non verrà nemmeno creato, sarà ottimizzato).

Tuttavia, l'ottimizzazione dipende dal compilatore per provare il comportamento indefinito, cosa che potrebbe non essere possibile fare. Considera questa semplice funzione all'interno di un file converter.cpp:

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

Quando il compilatore vede questa funzione, non sa se il puntatore è un puntatore nullo o meno. Quindi genera solo codice che trasforma qualsiasi puntatore nel riferimento corrispondente. (Btw: questo è un noop poiché puntatori e riferimenti sono la stessa identica bestia in assembler.) Ora, se hai un altro file user.cppcon il codice

#include "converter.h"

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

il compilatore non sa che toReference()dereferenzierà il puntatore passato e presume che restituisca un riferimento valido, che in pratica sarà un riferimento nullo. La chiamata riesce, ma quando si tenta di utilizzare il riferimento, il programma si blocca. Fiduciosamente. Lo standard consente che accada qualsiasi cosa, inclusa la comparsa di elefanti rosa.

Potresti chiederti perché questo è rilevante, dopo tutto, il comportamento indefinito è già stato attivato all'interno toReference(). La risposta è il debug: i riferimenti nulli possono propagarsi e proliferare proprio come fanno i puntatori nulli. Se non sei consapevole che possono esistere riferimenti nulli e impari a evitare di crearli, potresti dedicare un po 'di tempo a cercare di capire perché la tua funzione membro sembra bloccarsi quando sta solo cercando di leggere un semplice vecchio intmembro (risposta: l'istanza nella chiamata del membro era un riferimento nullo, quindi thisè un puntatore nullo e il tuo membro viene calcolato per essere posizionato come indirizzo 8).


Allora che ne dici di controllare i riferimenti nulli? Hai dato la linea

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

nella tua domanda. Bene, questo non funzionerà: secondo lo standard, hai un comportamento indefinito se dereferenzia un puntatore nullo e non puoi creare un riferimento nullo senza dereferenziare un puntatore nullo, quindi i riferimenti nulli esistono solo all'interno del regno del comportamento indefinito. Poiché il compilatore può presumere che non si stia innescando un comportamento indefinito, può presumere che non esista un riferimento nullo (anche se emetterà prontamente codice che genera riferimenti nulli!). In quanto tale, vede la if()condizione, conclude che non può essere vera e getta via l'intera if()affermazione. Con l'introduzione delle ottimizzazioni del tempo di collegamento, è diventato semplicemente impossibile controllare i riferimenti nulli in modo affidabile.


TL; DR:

I riferimenti nulli sono in qualche modo un'esistenza orribile:

La loro esistenza sembra impossibile (= per lo standard),
ma esistono (= per il codice macchina generato),
ma non puoi vederli se esistono (= i tuoi tentativi saranno ottimizzati),
ma potrebbero comunque ucciderti inconsapevole (= il tuo programma si blocca in punti strani o peggio).
La tua unica speranza è che non esistano (= scrivi il tuo programma per non crearli).

Spero che non venga a perseguitarti!


2
Cos'è esattamente un "ping elephant"?
Pharap

2
@Pharap non ho idea, era solo un errore di battitura. Ma allo standard C ++ non importerebbe se compaiono elefanti rosa o ping, comunque ;-)
cmaster - reinstalla monica

9

Se la tua intenzione era trovare un modo per rappresentare null in un'enumerazione di oggetti singleton, allora è una cattiva idea (de) fare riferimento a null (it C ++ 11, nullptr).

Perché non dichiarare un oggetto singleton statico che rappresenta NULL all'interno della classe come segue e aggiungere un operatore cast-to-pointer che restituisce nullptr?

Modifica: corretti diversi tipi di errore e aggiunta l'istruzione if in main () per verificare che l'operatore cast-to-pointer funzioni effettivamente (cosa che ho dimenticato di .. mio male) - 10 marzo 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;
  }
}

perché è lento
wandalen

6

clang ++ 3.5 avverte anche su di esso:

/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.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.