A referência nula é possível?


102

Este pedaço de código é válido (e comportamento definido)?

int &nullReference = *(int*)0;

Ambos g ++ e clang ++ compilação-lo sem qualquer aviso, mesmo quando se usa -Wall, -Wextra, -std=c++98, -pedantic, -Weffc++...

Claro que a referência não é realmente nula, uma vez que não pode ser acessada (significaria desreferenciar um ponteiro nulo), mas poderíamos verificar se é nulo ou não verificando seu endereço:

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

1
Você pode citar algum caso em que isso seja realmente útil? Em outras palavras, isso é apenas uma questão de teoria?
cdhowie

Bem, as referências são sempre indispensáveis? Ponteiros sempre podem ser usados ​​no lugar deles. Essa referência nula permitiria usar uma referência também quando você não pudesse ter nenhum objeto para se referir. Não sei o quanto é sujo, mas antes de pensar nisso me interessei pela sua legalidade.
peoro

8
Acho que é desaprovado
Padrão

22
"nós poderíamos verificar" - não, você não pode. Existem compiladores que transformam a instrução em if (false), eliminando a verificação, precisamente porque as referências não podem ser nulas de qualquer maneira. Uma versão melhor documentada existia no kernel Linux, onde uma verificação NULL muito semelhante foi otimizada: isc.sans.edu/diary.html?storyid=6820
MSalters

2
"uma das principais razões para usar uma referência em vez de um ponteiro é para livrá-lo do fardo de ter que testar para ver se ele se refere a um objeto válido" esta resposta, no link do Default, parece muito boa!
Peoro

Respostas:


75

As referências não são indicadores.

8.3.2 / 1:

Uma referência deve obrigatoriamente ser inicializada para se referir a um objeto ou função 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 vinculá-la ao “objeto” obtido pela desreferenciação de 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 Internacional como indefinidas (por exemplo, o efeito de desreferenciar o ponteiro nulo)

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


Removi minha resposta, pois percebi que o mero problema de desreferenciar um ponteiro nulo e obter um lvalue que se refere a isso é uma coisa diferente do que vincular uma referência a ele, como você mencionou. Embora se diga que lvalues ​​também se referem a objetos ou funções (portanto, neste ponto, realmente não há diferença para uma vinculação de referência), essas duas coisas ainda são preocupações separadas. Para o mero ato de desreferenciar, aqui está o link: open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102
Johannes Schaub - litb

1
@MSalters (resposta ao comentário sobre a resposta excluída; relevante aqui) Não posso concordar particularmente com a lógica apresentada aqui. Embora possa ser conveniente elidir &*pcomo puniversal, isso não exclui o comportamento indefinido (que por sua natureza pode "parecer funcionar"); e eu discordo que uma typeidexpressão que busca determinar o tipo de um "ponteiro nulo desreferenciado" realmente desreferencia o ponteiro nulo. Já vi pessoas argumentarem seriamente que &a[size_of_array]não se pode e não se deve confiar e, de qualquer forma, é mais fácil e seguro apenas escrever a + size_of_array.
Karl Knechtel

Os padrões @Default nas tags [c ++] devem ser altos. Minha resposta soou como se os dois atos fossem a mesma coisa :) Enquanto desreferenciar e obter um valor que você não repassa que se refere a "nenhum objeto" poderia ser viável, armazená-lo em uma referência escapa desse escopo limitado e de repente pode impactar muito mais código.
Johannes Schaub - litb

@Karl bem em C ++, "desreferenciar" não significa ler um valor. Algumas pessoas pensam que "desreferenciar" significa realmente acessar ou modificar o valor armazenado, mas isso não é verdade. A lógica é que C ++ diz que um lvalue se refere a "um objeto ou função". Se for assim, então a questão é a que lvalue *pse refere, quando pé um ponteiro nulo. C ++ atualmente não tem a noção de um lvalue vazio, que a edição 232 queria introduzir.
Johannes Schaub - litb

Detecção de ponteiros nulos não referenciados em typeidtrabalhos baseados na sintaxe, em vez de baseados na semântica. Ou seja, se você faz typeid(0, *(ostream*)0)você faz tem um comportamento indefinido - não bad_typeidé garantido para ser jogado, mesmo que você passar um lvalue resultante de um ponteiro dereference nulo semanticamente. Mas sintaticamente no nível superior, não é uma desreferência, mas uma expressão de operador vírgula.
Johannes Schaub - litb

26

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.cppcom 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 intmembro 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!


2
O que exatamente é um "elefante ping"?
Pharap de

2
@Pharap Não tenho ideia, foi apenas um erro de digitação. Mas o padrão C ++ não se importaria se é rosa ou
pingado

9

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

Por que não declarar o objeto singleton estático que representa NULL dentro da classe como segue e adicionar um operador cast-to-pointer que retorna nullptr?

Editar: vários tipos de erros corrigidos e adição de instrução if em main () para testar o funcionamento do operador cast-to-pointer (que esqueci de ... meu erro) - 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<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;
  }
}

porque é lento
wandalen

6

o clang ++ 3.5 ainda avisa sobre isso:

/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.
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.