Referência nula é possível?

Esta parte do código é válida (e comportamento definido)?

int &nullReference = *(int*)0; 

Tanto o g + + como o clang + + compilam-no sem qualquer aviso, mesmo quando utiliza o -Wextra , -std=c++98 , -std=c++98 , -pedantic , -Weffc++

É claro que a referência não é realmente nula, já que ela não pode ser acessada (isso significaria desreferenciar um ponteiro nulo), mas poderíamos verificar se ela é nula ou não verificando seu endereço:

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

Referências não são pointers.

8.3.2 / 1:

Uma referência deve ser inicializada para se referir a um object ou function válida. [Nota: em particular, uma referência nula não pode existir em um programa bem definido, porque a única maneira de criar tal referência seria ligá-lo ao “object” obtido por desreferenciando um ponteiro nulo, o que causa um comportamento indefinido. Conforme descrito em 9.6, uma referência não pode ser vinculada diretamente a um campo de bits. ]

1,9 / 4:

Certas outras operações são descritas nesta Norma como indefinida (por exemplo, o efeito de desreferenciar o ponteiro nulo)

Como Johannes diz em uma resposta excluída, há algumas dúvidas se “desreferenciando um ponteiro nulo” deve ser categoricamente declarado como comportamento indefinido. Mas este não é um dos casos que levantam dúvidas, já que um ponteiro nulo certamente não aponta para um “object ou function válida”, e não há desejo dentro do comitê de padrões de introduzir referências nulas.

Se sua intenção era encontrar uma maneira de representar nulo em uma enumeração de objects singleton, então é uma má ideia (de) referenciar null (é C ++ 11, nullptr).

Por que não declarar object singleton estático que representa NULL dentro da class da seguinte maneira e adicionar um operador de conversão para ponteiro que retorna nullptr?

Edit: Corrigido vários mistypes e adicionado if-statement em main () para testar o operador cast-to-pointer realmente trabalhando (que eu esqueci de .. meu mal) – 10 de março 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> _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> Error::_instances; Error& Error::NewInstance(const string& name, bool isNull = false) { shared_ptr 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; } } 

clang ++ 3.5 até avisa:

 /tmp/aC: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. 

A resposta depende do seu ponto de vista:


Se você julgar pelo padrão C ++, não poderá obter uma referência nula porque terá um comportamento indefinido primeiro. Após essa primeira incidência de comportamento indefinido, o padrão permite que qualquer coisa aconteça. Então, se você escrever *(int*)0 , você já tem um comportamento indefinido como é, do ponto de vista do padrão da linguagem, desreferenciando um ponteiro nulo. O resto do programa é irrelevante, uma vez que esta expressão é executada, você está fora do jogo.


No entanto, na prática, referências nulas podem ser facilmente criadas a partir de pointers nulos, e você não notará até que você realmente tente acessar o valor por trás da referência nula. Seu exemplo pode ser um pouco simples demais, já que 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, será otimizada).

No entanto, essa otimização depende do compilador para provar o comportamento indefinido, o que pode não ser possível. Considere esta function simples dentro de um arquivo converter.cpp :

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

Quando o compilador vê essa function, ele não sabe se o ponteiro é um ponteiro nulo ou não. Por isso, apenas gera código que transforma qualquer ponteiro na referência correspondente. (Btw: Este é um noop, já que os pointers e referências são exatamente o mesmo monstro 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() irá desreferenciar o ponteiro passado, e assume que ele retorna uma referência válida, que por acaso será uma referência nula na prática. 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 a aparência de elefantes de ping.

Você pode perguntar por que isso é relevante, afinal, o comportamento indefinido já foi acionado dentro de toReference() . A resposta é a debugging: as referências nulas podem se propagar e proliferar como fazem os pointers nulos. Se você não está ciente de que referências nulas podem existir e aprende a evitar criá-las, você pode gastar algum tempo tentando descobrir por que sua function membro parece travar quando está apenas tentando ler um membro int antigo comum (resposta: instância na chamada do membro era uma referência nula, portanto, this é um ponteiro nulo e seu membro é computado para ser localizado como endereço 8).


Então, que tal verificar referências nulas? Você deu a linha

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

na sua pergunta. Bem, isso não funcionará: de acordo com o padrão, você tem um comportamento indefinido se não mencionar um ponteiro nulo e não pode criar uma referência nula sem desreferenciar um ponteiro nulo, portanto, as referências nulas só existem dentro do domínio do comportamento indefinido. Uma vez que seu compilador pode assumir que você não está desencadeando um comportamento indefinido, ele pode assumir que não existe uma referência nula (mesmo que ele emita prontamente código que gere referências nulas!). Como tal, ele vê a condição if() , conclui que não pode ser verdade, e simplesmente joga fora toda a instrução if() . Com a introdução de otimizações de tempo de link, tornou-se impossível verificar se há referências nulas de maneira robusta.


TL; DR:

Referências nulas são um pouco de uma existência espantosa:

Sua existência 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 inconsciente 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 não venha assombrá-lo!

    Intereting Posts