Возможна ли нулевая ссылка? - PullRequest
86 голосов
/ 06 декабря 2010

Является ли этот фрагмент кода допустимым (и определенное поведение)?

int &nullReference = *(int*)0;

Как g ++, так и clang ++ компилируют его без предупреждения, даже при использовании -Wall, -Wextra, -std=c++98, -pedantic, -Weffc++ ...

Конечно, ссылка на самом деле не является нулевой, поскольку к ней нельзя получить доступ (это будет означать разыменование нулевого указателя), но мы можем проверить, является ли она нулевой или нет, проверив ее адрес:

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

Ответы [ 4 ]

67 голосов
/ 06 декабря 2010

Ссылки не являются указателями.

8.3.2 / 1:

Ссылка должна быть инициализирована обратитесь к действительному объекту или функции. [Примечание: в частности, нулевая ссылка не может существовать в четко определенном программа, потому что единственный способ создать такую ​​ссылку будет привязать его к «объекту», полученному разыменование нулевого указателя, который вызывает неопределенное поведение. Как описанный в 9.6, ссылка не может быть привязанным непосредственно к битовому полю. ]

1,9 / 4

описаны некоторые другие операции в этом международном стандарте, как неопределенный (например, эффект разыменование нулевого указателя)

Как сказал Йоханнес в удаленном ответе, есть некоторые сомнения в том, что «разыменование нулевого указателя» должно быть категорически объявлено как неопределенное поведение. Но это не один из случаев, который вызывает сомнения, поскольку нулевой указатель, безусловно, не указывает на «действительный объект или функцию», и в комитете по стандартизации нет желания вводить нулевые ссылки.

12 голосов
/ 01 марта 2017

Ответ зависит от вашей точки зрения:


Если вы судите по стандарту C ++, вы не можете получить нулевую ссылку, потому что сначала вы получаете неопределенное поведение. После этого первого случая неопределенного поведения стандарт позволяет всему происходить. Таким образом, если вы пишете *(int*)0, у вас уже есть неопределенное поведение, поскольку вы, с точки зрения языка, разыменовываете нулевой указатель. Остальная часть программы не имеет значения, после выполнения этого выражения вы выходите из игры.


Однако на практике нулевые ссылки могут быть легко созданы из нулевых указателей, и вы не заметите, пока не попытаетесь получить доступ к значению за нулевой ссылкой. Ваш пример может быть слишком простым, так как любой хороший оптимизирующий компилятор увидит неопределенное поведение и просто оптимизирует все, что от него зависит (нулевая ссылка даже не будет создана, она будет оптимизирована).

Тем не менее, эта оптимизация зависит от компилятора, чтобы доказать неопределенное поведение, что может оказаться невозможным. Рассмотрим эту простую функцию внутри файла converter.cpp:

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

Когда компилятор видит эту функцию, он не знает, является ли указатель нулевым или нет. Так что он просто генерирует код, который превращает любой указатель в соответствующую ссылку. (Между прочим, это не что иное, поскольку указатели и ссылки в ассемблере точно такие же, как у зверя.) Теперь, если у вас есть другой файл user.cpp с кодом

#include "converter.h"

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

компилятор не знает, что toReference() будет разыменовывать переданный указатель, и предположит, что он возвращает действительную ссылку, которая на практике окажется пустой ссылкой. Вызов завершается успешно, но когда вы пытаетесь использовать ссылку, программа вылетает. С надеждой. Стандарт допускает все, что угодно, включая появление пинговых слонов.

Вы можете спросить, почему это важно, ведь неопределенное поведение уже было запущено внутри toReference(). Ответ отладочный: нулевые ссылки могут распространяться и распространяться так же, как нулевые указатели. Если вы не знаете, что нулевые ссылки могут существовать, и учитесь избегать их создания, вы можете потратить довольно много времени, пытаясь выяснить, почему ваша функция-член кажется сбойной, когда она просто пытается прочитать обычный старый int член (ответ : экземпляр в вызове члена был пустой ссылкой, поэтому this является нулевым указателем, и ваш член вычисляется как адрес 8).


Так как насчет проверки на нулевые ссылки? Вы дали строку

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

в вашем вопросе. Что ж, это не сработает: в соответствии со стандартом, у вас неопределенное поведение, если вы разыменовываете нулевой указатель, и вы не можете создать нулевую ссылку, не разыменовав нулевой указатель, поэтому нулевые ссылки существуют только внутри области неопределенного поведения. Поскольку ваш компилятор может предполагать, что вы не запускаете неопределенное поведение, он может предполагать, что такой вещи, как нулевая ссылка, не существует (даже если он с готовностью генерирует код, генерирующий нулевые ссылки!). Таким образом, он видит условие if(), делает вывод, что оно не может быть истинным, и просто отбрасывает весь оператор if(). С введением оптимизации времени ссылки стало невозможным надежно проверять нулевые ссылки.


TL; DR:

Нулевые ссылки являются чем-то ужасным:

Их существование кажется невозможным (= по стандарту),
но они существуют (= по сгенерированному машинному коду),
но вы не можете видеть их, если они существуют (= ваши попытки будут оптимизированы),
но в любом случае они могут убить вас, даже не подозревая (= ваша программа падает в странных точках или еще хуже).
Ваша единственная надежда состоит в том, что они не существуют (= напишите вашу программу, чтобы не создавать их).

Надеюсь, это вас не преследует!

8 голосов
/ 22 ноября 2012

Если вы намеревались найти способ представить null в перечислении одноэлементных объектов, то плохая идея (де) ссылаться на null (это C ++ 11, nullptr).

Почему бы не объявить статический одноэлементный объект, который представляет NULL в классе, следующим образом и добавить оператор приведения к указателю, который возвращает nullptr?

Редактировать: Исправлено несколько опечаток и добавлен оператор if в main () для проверки работающего оператора приведения к указателю (что я забыл .. мой плохой) - 10 марта 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;
  }
}
6 голосов
/ 20 декабря 2014

clang ++ 3.5 даже предупреждает об этом:

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