A resposta depende do seu ponto de vista:
Se você julgar pelo padrão C ++, não poderá obter uma referência nula porque obtém o comportamento indefinido primeiro. Depois dessa primeira incidência de comportamento indefinido, o padrão permite que tudo aconteça. Então, se você escreve *(int*)0
, você já tem um comportamento indefinido, do ponto de vista do padrão da linguagem, desreferenciando um ponteiro nulo. O resto do programa é irrelevante, uma vez que esta expressão seja executada, você está fora do jogo.
No entanto, na prática, as referências nulas podem ser facilmente criadas a partir de ponteiros nulos e você não notará até que realmente tente acessar o valor por trás da referência nula. Seu exemplo pode ser um pouco simples demais, pois qualquer bom compilador de otimização verá o comportamento indefinido e simplesmente otimizará qualquer coisa que dependa dele (a referência nula nem será criada, ela será otimizada).
No entanto, essa otimização depende do compilador para provar o comportamento indefinido, o que pode não ser possível fazer. Considere esta função simples dentro de um arquivo converter.cpp
:
int& toReference(int* pointer) {
return *pointer;
}
Quando o compilador vê essa função, ele não sabe se o ponteiro é um ponteiro nulo ou não. Portanto, ele apenas gera código que transforma qualquer ponteiro na referência correspondente. (Btw: Este é um noop, pois ponteiros e referências são exatamente a mesma besta no assembler.) Agora, se você tiver outro arquivo user.cpp
com o código
#include "converter.h"
void foo() {
int& nullRef = toReference(nullptr);
cout << nullRef; //crash happens here
}
o compilador não sabe que toReference()
cancelará a referência ao ponteiro passado e presumirá que ele retornará uma referência válida, que na prática será uma referência nula. A chamada é bem-sucedida, mas quando você tenta usar a referência, o programa falha. Esperançosamente. O padrão permite que qualquer coisa aconteça, incluindo o aparecimento de elefantes rosa.
Você pode perguntar por que isso é relevante, afinal, o comportamento indefinido já foi acionado internamente toReference()
. A resposta é depuração: referências nulas podem se propagar e proliferar da mesma forma que os ponteiros nulos fazem. Se você não está ciente de que podem existir referências nulas e aprende a evitar criá-las, você pode passar algum tempo tentando descobrir por que sua função de membro parece travar quando está apenas tentando ler um int
membro antigo comum (resposta: a instância na chamada do membro havia uma referência nula, então this
é um ponteiro nulo, e seu membro é calculado para estar localizado como endereço 8).
Então, que tal verificar referências nulas? Você deu a linha
if( & nullReference == 0 ) // null reference
em sua pergunta. Bem, isso não funcionará: de acordo com o padrão, você tem um comportamento indefinido se desreferenciar um ponteiro nulo e não pode criar uma referência nula sem desreferenciar um ponteiro nulo, portanto, as referências nulas existem apenas dentro do domínio do comportamento indefinido. Visto que seu compilador pode assumir que você não está disparando um comportamento indefinido, ele pode assumir que não existe uma referência nula (embora ele emita prontamente um código que gera referências nulas!). Como tal, ele vê a if()
condição, conclui que não pode ser verdade e apenas joga fora a if()
afirmação inteira . Com a introdução de otimizações de tempo de link, tornou-se totalmente impossível verificar referências nulas de uma maneira robusta.
TL; DR:
Referências nulas são um tanto quanto uma existência horrível:
A existência deles parece impossível (= pelo padrão),
mas eles existem (= pelo código de máquina gerado),
mas você não pode vê-los se eles existirem (= suas tentativas serão otimizadas),
mas eles podem matá-lo sem saber de qualquer maneira (= seu programa trava em pontos estranhos, ou pior).
Sua única esperança é que eles não existam (= escreva seu programa para não criá-los).
Espero que isso não venha a assombrá-lo!