¿Es posible una referencia nula?
¿Es este fragmento de código válido (y comportamiento definido)?
int &nullReference = *(int*)0;
Tanto g++ como clang++ lo compilan sin previo aviso, incluso cuando se usa -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
Las referencias no son indicadores.
8.3.2/1:
Se inicializará una referencia para hacer referencia a un objeto o función válido. [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 eliminar la referencia a un puntero nulo, lo que provoca un comportamiento indefinido. Como se describe en 9.6, una referencia no puede vincularse directamente a un campo de bits. ]
1,9/4:
Ciertas otras operaciones se describen en esta norma internacional como indefinidas (por ejemplo, el efecto de eliminar la referencia al puntero nulo).
Como dice Johannes en una respuesta eliminada, existen dudas sobre si "eliminar la referencia a un puntero nulo" debe considerarse 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álido", y no hay ningún deseo dentro del comité de estándares de introducir referencias nulas.
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 ya que, desde el punto de vista del estándar del lenguaje, está desreferenciando un puntero nulo. El resto del programa es irrelevante, una vez ejecutada 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 lo notarás hasta que intentes 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, esa optimización depende de que el compilador demuestre el comportamiento indefinido, lo que puede no ser posible de hacer. 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 nulo o no. Entonces simplemente 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.cpp
con 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 al puntero pasado y asumirá que devuelve una referencia válida, que en la práctica resultará ser una referencia nula. La llamada se realiza correctamente, pero cuando intenta utilizar la referencia, el programa falla. Con un poco de suerte. El estándar permite que suceda cualquier cosa, incluida la aparición de elefantes rosas.
Quizás se pregunte por qué esto es relevante; después de todo, el comportamiento indefinido ya se activó en el interior toReference()
. La respuesta es la depuración: las referencias nulas pueden propagarse y proliferar tal como lo hacen los punteros nulos. Si no es consciente de que pueden existir referencias nulas y aprende a evitar crearlas, puede pasar bastante tiempo tratando de descubrir por qué su función miembro parece fallar cuando simplemente intenta leer un int
miembro antiguo simple (respuesta: la instancia en la llamada del miembro había una referencia nula, también lo this
es un puntero nulo, y se calcula que su miembro está ubicado como la dirección 8).
Entonces, ¿qué tal si buscamos referencias nulas? Tú diste la línea
if( & nullReference == 0 ) // null reference
en tu pregunta. Bueno, eso no funcionará: según el estándar, tiene un comportamiento indefinido si elimina la referencia a un puntero nulo, y no puede crear una referencia nula sin eliminar la referencia a 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á desencadenando 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 cierta y simplemente descarta toda la if()
afirmación. Con la introducción de optimizaciones del tiempo de enlace, se ha vuelto completamente imposible verificar referencias nulas de manera sólida.
TL;DR:
Las referencias nulas son 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 puedes verlos si existen (= tus intentos serán optimizados),
pero pueden matarte sin que te des cuenta de todos modos (= su programa falla en puntos extraños, o algo peor).
Su única esperanza es que no existan (= escriba su programa para no crearlos).
¡Espero que eso no te persiga!
clang++ 3.5 incluso advierte al respecto:
/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.