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

Вопрос или проблема

Этот фрагмент кода валиден (и поведение определено)?

int &nullReference = *(int*)0;

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

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

if( & nullReference == 0 ) // нулевая ссылка

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

8.3.2/1:

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

1.9/4:

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

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

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


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


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

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

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

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

#include "converter.h"

void foo() {
    int& nullRef = toReference(nullptr);
    cout << nullRef;    // крах происходит здесь
}

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

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


Итак, как насчет проверки на нулевые ссылки? Вы привели строку:

if( & nullReference == 0 ) // нулевая ссылка

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


Итого:

Нулевые ссылки представляют собой нечто ужасное:

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

Я надеюсь, что это не станет причиной ваших страданий!

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 ) // нулевая ссылка
      ^~~~~~~~~~~~~    ~
1 warning generated.

Если ваше намерение заключалось в том, чтобы найти способ представить 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; // синглтон-объект, представляющий 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"); Правка: исправлено
//Error& Error::NOT_FOUND = Error::NewInstance("NONE", true); Правка: исправлено
Error& Error::UNKNOWN = Error::NewInstance("UNKNOWN");
Error& Error::NONE = Error::NewInstance("NONE");

// Main.cpp
#include "Error.h"

Error& getError() {
  return Error::UNKNOWN;
}

// Правка: Чтобы увидеть перегрузку "Error*()" в Error.h, работающей 
Error& getErrorNone() {
  return Error::NONE;
}

int main(void) {
  if(getError() != Error::NONE) {
    return EXIT_FAILURE;
  }

  // Правка: Чтобы увидеть перегрузку "Error*()" в Error.h, работающей 
  if(getErrorNone() != nullptr) {
    return EXIT_FAILURE;
  }
}

Ответ или решение

Возможен ли нулевой (null) референс в C++?

Вопрос о том, возможен ли нулевой референс в C++, требует глубокого понимания концепций ссылок и указателей, а также особенностей стандартов языка. Разберем последовательно все аспекты.

1. Программа и поведение

Рассмотрим следующий код:

int &nullReference = *(int*)0;

На первый взгляд, код компилируется без предупреждений как в g++, так и в clang++, даже при использовании опций, которые включают строгую проверку. Тем не менее, важным является то, что попытка разыменования null-указателя ((int*)0) ведет к неопределенному поведению.

Согласно стандарту C++ (раздел 8.3.2/1):

Ссылка должна быть инициализирована так, чтобы ссылаться на действительный объект или функцию. Обратите внимание: в частности, нулевой референс не может существовать в хорошо определенной программе, потому что единственный способ создать такой референс — это связать его с ‘объектом’, полученным при разыменовании нулевого указателя, что приводит к неопределенному поведению.

2. Неправильность использования

При превращении нулевого указателя в ссылку:

if (&nullReference == 0) // проверка на null-ссылку

мы снова получаем неопределенное поведение. Стандарт не допускает создание нулевых референсов, так как вся логика основана на предположении, что ссылки всегда должны указывать на действительные объекты.

3. Влияние оптимизации компилятора

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

4. Практика и реальность

Практически вы можете столкнуться с ситуацией, когда создается нулевой референс, особенно если код не проверяет параметры перед их использованием. Например:

int& toReference(int* pointer) {
    return *pointer; // возможен разыменование нулевого указателя
}

void foo() {
    int& nullRef = toReference(nullptr);
    std::cout << nullRef; // сбой в работе программы
}

В этом примере, если toReference вызывается с нулевым указателем, то возвращаемая ссылка будет указывать на неопределенный объект, и попытка доступа к ней приведет к сбою программы.

5. Выводы

  • Согласно стандарту C++: Нулевые ссылки не могут существовать, так как они вызывают неопределенное поведение при их создании.
  • На практике: Хотя в результате неаккуратного кода можно создать нулевые ссылки, это приведет к сбоям при доступе к ним, что делает вашу программу нестабильной и ненадежной.
  • Устранение проблем: Для избежания таких ситуаций рекомендуется избегать разыменования указателей, не проверенных на нуль, и применять механизмы обработки ошибок, чтобы убедиться, что ссылка всегда ссылается на валидный объект.

Таким образом, хотя технически неразрешимая ссылка можно создать, с точки зрения стандарта и безопасной практики это состояние следует полностью избегать.

Оцените материал
Добавить комментарий

Капча загружается...