Возможна ли нулевая ссылка?


104

Действителен ли этот фрагмент кода (и определено ли поведение)?

int &nullReference = *(int*)0;

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

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

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

1
Можете ли вы привести случай, когда это действительно было бы полезно? Другими словами, это всего лишь вопрос теории?
cdhowie 06

Ну а ссылки когда-нибудь необходимы? Вместо них всегда можно использовать указатели. Такая пустая ссылка позволит вам использовать ссылку даже тогда, когда у вас не может быть объекта для ссылки. Не знаю, насколько он грязный, но прежде чем подумать, меня интересовала его законность.
peoro 06

8
Я думаю, что это неодобрительно
По умолчанию

22
«мы могли бы проверить» - нет, нельзя. Существуют компиляторы, которые превращают оператор в if (false), исключая проверку, именно потому, что ссылки в любом случае не могут быть нулевыми. Лучшая документированная версия существовала в ядре Linux, где была оптимизирована очень похожая проверка NULL: isc.sans.edu/diary.html?storyid=6820
MSalters

2
«одна из основных причин использовать ссылку вместо указателя - освободить вас от бремени тестирования, чтобы увидеть, ссылается ли он на действительный объект», этот ответ в ссылке по умолчанию звучит довольно хорошо!
peoro 06

Ответы:


76

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

8.3.2 / 1:

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

1.9 / 4:

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

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


Я удалил свой ответ, так как понял, что простая проблема разыменования нулевого указателя и получения относящегося к нему lvalue - это совсем другое дело, чем фактическая привязка ссылки к нему, как вы упомянули. Хотя считается, что lvalue также относится к объектам или функциям (так что в данном случае нет никакой разницы в привязке ссылки), эти две вещи по-прежнему представляют собой разные проблемы. Для простого разыменования вот ссылка: open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1102
Johannes Schaub - litb

1
@MSalters (ответ на комментарий к удаленному ответу; актуально здесь) Я не могу особо согласиться с представленной там логикой. Хотя может быть удобно исключить &*pкак pуниверсальное, это не исключает неопределенного поведения (которое по своей природе может «показаться работающим»); и я не согласен с тем, что typeidвыражение, которое пытается определить тип «разыменованного нулевого указателя», фактически разыменовывает нулевой указатель. Я видел, как люди серьезно спорят о том, что на них &a[size_of_array]нельзя и не следует полагаться, и в любом случае проще и безопаснее просто писать a + size_of_array.
Карл Кнехтель

@Default Стандарты в тегах [c ++] должны быть высокими. Мой ответ звучал так, как будто оба действия были одним и тем же :) Хотя разыменование и получение lvalue, которое вы не передаете, которое относится к «без объекта», возможно, сохранение его в ссылке позволяет избежать этой ограниченной области и внезапно может повлиять на гораздо больше кода.
Йоханнес Шауб - литб

@Karl в C ++ «разыменование» не означает считывание значения. Некоторые люди думают, что «разыменование» означает доступ к сохраненному значению или его изменение, но это не так. Логика такова, что C ++ говорит, что lvalue относится к «объекту или функции». Если это так, то вопрос в том, на что *pссылается lvalue , когда pэто нулевой указатель. В C ++ в настоящее время нет понятия пустого lvalue, которое хотел представить проблема 232.
Йоханнес Шауб - литб

Обнаружение разыменованных нулевых указателей в typeidработе на основе синтаксиса, а не на основе семантики. То есть, если у typeid(0, *(ostream*)0)вас действительно есть неопределенное поведение - не bad_typeidгарантируется, что будет выбрано no , даже если вы семантически передаете lvalue, возникающее в результате разыменования нулевого указателя. Но синтаксически на верхнем уровне это не разыменование, а выражение оператора запятой.
Йоханнес Шауб - литб

27

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


Если вы судите по стандарту 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:

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

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

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


2
Что такое «слоник пинга»?
Pharap

2
@Pharap Понятия не имею, это была опечатка. Но стандарту C ++ все равно будет все равно, появятся ли они розовые или звонкие слоны ;-)
cmaster -

9

Если вы намеревались найти способ представления 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;
  }
}

потому что это медленно
wandalen

6

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.
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.