¿Es posible una referencia nula?

102

¿Es este código válido (y comportamiento definido)?

int &nullReference = *(int*)0;

Tanto g ++ y sonido metálico ++ compilación sin ninguna advertencia, incluso cuando se utilizan -Wall, -Wextra, -std=c++98, -pedantic, -Weffc++...

Por supuesto, la referencia no es realmente nula, ya que no se puede acceder a ella (significaría desreferenciar un puntero nulo), pero podríamos comprobar si es nula o no comprobando su dirección:

if( & nullReference == 0 ) // null reference
peoro
fuente
1
¿Puede darnos algún caso en el que esto sea realmente útil? En otras palabras, ¿es solo una cuestión de teoría?
cdhowie
Bueno, ¿las referencias son alguna vez indispensables? Siempre se pueden usar punteros en lugar de ellos. Una referencia tan nula le permitiría usar una referencia también cuando no pudiera tener ningún objeto al que hacer referencia. No sé lo sucio que está, pero antes de pensarlo me interesó su legalidad.
peoro
8
Creo que está mal visto
predeterminado
22
"Podríamos comprobar" - no, no puedes. Hay compiladores que convierten la declaración en if (false), eliminando el cheque, precisamente porque las referencias no pueden ser nulas de todos modos. Existía una versión mejor documentada en el kernel de Linux, donde se optimizó una verificación NULL muy similar: isc.sans.edu/diary.html?storyid=6820
MSalters
2
"una de las principales razones para usar una referencia en lugar de un puntero es liberarte de la carga de tener que probar para ver si se refiere a un objeto válido" esta respuesta, en el enlace de Default, ¡suena bastante bien!
peoro

Respuestas:

75

Las referencias no son indicadores.

8.3.2 / 1:

Se debe inicializar una referencia para hacer referencia a un objeto o función válida. [Nota: en particular, una referencia nula no puede existir en un programa bien definido, porque la única forma de crear dicha referencia sería vincularla al "objeto" obtenido al desreferenciar un puntero nulo, lo que provoca un comportamiento indefinido. Como se describe en 9.6, una referencia no se puede vincular directamente a un campo de bits. ]

1,9 / 4:

Algunas otras operaciones se describen en esta Norma Internacional como indefinidas (por ejemplo, el efecto de desreferenciar el puntero nulo)

Como dice Johannes en una respuesta eliminada, hay algunas dudas sobre si "desreferenciar un puntero nulo" debe declararse categóricamente como un comportamiento indefinido. Pero este no es uno de los casos que generan dudas, ya que un puntero nulo ciertamente no apunta a un "objeto o función válida", y no hay ningún deseo dentro del comité de estándares de introducir referencias nulas.

Steve Jessop
fuente
Eliminé mi respuesta porque me di cuenta de que el mero problema de eliminar la referencia a un puntero nulo y obtener un valor l que se refiere a eso es algo diferente a vincular una referencia a él, como mencionas. Aunque se dice que lvalues ​​también se refieren a objetos o funciones (por lo que en este punto, realmente no hay una diferencia con un enlace de referencia), estas dos cosas siguen siendo preocupaciones separadas. Para el mero acto de desreferenciar, aquí está el enlace: open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102
Johannes Schaub - litb
1
@MSalters (responda al comentario sobre la respuesta eliminada; relevante aquí) No puedo estar particularmente de acuerdo con la lógica presentada allí. Si bien puede ser conveniente elidir &*pcomo puniversalmente, eso no descarta un comportamiento indefinido (que por su naturaleza puede "parecer funcionar"); y no estoy de acuerdo con que una typeidexpresión que busca determinar el tipo de un "puntero nulo desreferenciado" en realidad desreferencia al puntero nulo. He visto a gente discutir seriamente que &a[size_of_array]no se puede ni se debe confiar en ellos y, de todos modos, es más fácil y seguro escribir a + size_of_array.
Karl Knechtel
@ Los estándares predeterminados en las etiquetas [c ++] deben ser altos. Mi respuesta sonó como si ambos actos fueran una y la misma cosa :) Si bien desreferenciar y obtener un lvalor que no se transmite que se refiere a "ningún objeto" podría ser factible, almacenarlo en una referencia escapa a ese alcance limitado y de repente podría impactar mucho más código.
Johannes Schaub - litb
@Karl Bueno, en C ++, "desreferenciar" no significa leer un valor. Algunas personas piensan que "desreferenciar" significa acceder o modificar realmente el valor almacenado, pero eso no es cierto. La lógica es que C ++ dice que un lvalue se refiere a "un objeto o función". Si es así, entonces la pregunta es a qué se *prefiere lvalue , cuándo pes un puntero nulo. C ++ actualmente no tiene la noción de un lvalue vacío, que el problema 232 quería introducir.
Johannes Schaub - litb
Detección de punteros nulos desreferenciados en typeidtrabajos basados ​​en sintaxis, en lugar de basados ​​en semántica. Es decir, si lo hace typeid(0, *(ostream*)0)usted lo hace tener un comportamiento indefinido - no bad_typeidse garantiza que sea lanzado, a pesar de que se pasa un valor izquierdo como resultado de una referencia de puntero nulo semánticamente. Pero sintácticamente en el nivel superior, no es una desreferencia, sino una expresión de operador de coma.
Johannes Schaub - litb
26

La respuesta depende de tu punto de vista:


Si juzga por el estándar C ++, no puede obtener una referencia nula porque primero obtiene un comportamiento indefinido. Después de esa primera incidencia de comportamiento indefinido, el estándar permite que suceda cualquier cosa. Entonces, si escribe *(int*)0, ya tiene un comportamiento indefinido como lo es, desde el punto de vista estándar del lenguaje, desreferenciar un puntero nulo. El resto del programa es irrelevante, una vez que se ejecuta esta expresión, estás fuera del juego.


Sin embargo, en la práctica, las referencias nulas se pueden crear fácilmente a partir de punteros nulos y no se dará cuenta hasta que intente acceder al valor detrás de la referencia nula. Su ejemplo puede ser demasiado simple, ya que cualquier buen compilador de optimización verá el comportamiento indefinido y simplemente optimizará todo lo que dependa de él (la referencia nula ni siquiera se creará, se optimizará).

Sin embargo, la optimización depende de que el compilador demuestre el comportamiento indefinido, lo que puede no ser posible. Considere esta función simple dentro de un archivo converter.cpp:

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

Cuando el compilador ve esta función, no sabe si el puntero es un puntero nulo o no. Entonces solo genera código que convierte cualquier puntero en la referencia correspondiente. (Por cierto: esto es un error ya que los punteros y las referencias son exactamente la misma bestia en ensamblador). Ahora, si tiene otro archivo user.cppcon el código

#include "converter.h"

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

el compilador no sabe que toReference()eliminará la referencia del puntero pasado y asumirá que devuelve una referencia válida, que resultará ser una referencia nula en la práctica. La llamada se realiza correctamente, pero cuando intenta utilizar la referencia, el programa se bloquea. Ojalá. El estándar permite que suceda cualquier cosa, incluida la aparición de elefantes rosados.

Puede preguntarse por qué esto es relevante, después de todo, el comportamiento indefinido ya se desencadenó en el interior toReference(). La respuesta es depurar: las referencias nulas pueden propagarse y proliferar al igual que lo hacen los punteros nulos. Si no sabe que pueden existir referencias nulas y aprende a evitar crearlas, puede pasar bastante tiempo tratando de averiguar por qué su función de miembro parece fallar cuando solo intenta leer un intmiembro antiguo simple (respuesta: la instancia en la llamada del miembro era una referencia nula, por lo que thises un puntero nulo, y su miembro se calcula para ubicarse como dirección 8).


Entonces, ¿qué tal si verificamos referencias nulas? Le diste la linea

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

en tu pregunta. Bueno, eso no funcionará: de acuerdo con el estándar, tiene un comportamiento indefinido si desreferencia un puntero nulo, y no puede crear una referencia nula sin desreferenciar un puntero nulo, por lo que las referencias nulas existen solo dentro del ámbito del comportamiento indefinido. Dado que su compilador puede asumir que no está activando un comportamiento indefinido, puede asumir que no existe una referencia nula (¡aunque emitirá fácilmente código que genera referencias nulas!). Como tal, ve la if()condición, concluye que no puede ser verdadera y simplemente desecha toda la if()afirmación. Con la introducción de optimizaciones de tiempo de enlace, se ha vuelto simplemente imposible verificar referencias nulas de una manera sólida.


TL; DR:

Las referencias nulas tienen una existencia algo espantosa:

Su existencia parece imposible (= según el estándar),
pero existen (= según el código de máquina generado),
pero no puede verlos si existen (= sus intentos se optimizarán),
pero de todos modos pueden matarlo sin darse cuenta (= su programa se bloquea en puntos extraños o peores).
Tu única esperanza es que no existan (= escribe tu programa para no crearlos).

¡Espero que eso no te persiga!

cmaster - reinstalar a monica
fuente
2
¿Qué es exactamente un "ping elefante"?
Pharap
2
@Pharap No tengo ni idea, fue solo un error tipográfico. Pero al estándar C ++ no le importaría si son elefantes rosados ​​o ping los que aparecen, de todos modos ;-)
cmaster - reinstate monica
9

Si su intención era encontrar una manera de representar nulo en una enumeración de objetos singleton, entonces es una mala idea (des) hacer referencia a nulo (es C ++ 11, nullptr).

¿Por qué no declarar un objeto singleton estático que representa NULL dentro de la clase de la siguiente manera y agregar un operador de conversión a puntero que devuelve nullptr?

Editar: se corrigieron varios tipos incorrectos y se agregó una declaración if en main () para probar si el operador de conversión a puntero funcionaba realmente (que olvidé ... mi mal) - 10 de marzo de 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
fuente
porque es lento
wandalen
6

clang ++ 3.5 incluso advierte sobre ello:

/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
fuente