Чи можлива нульова посилання?


102

Чи дійсний цей фрагмент коду (та визначена поведінка)?

int &nullReference = *(int*)0;

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

Звичайно, посилання насправді не є нульовим, оскільки до нього не можна отримати доступ (це означатиме перенаправлення нульового вказівника), але ми можемо перевірити, чи є це нульовим чи ні, перевіривши його адресу:

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

1
Чи можете ви навести якийсь випадок, коли це насправді було б корисно? Іншими словами, це лише питання теорії?
cdhowie

Ну, чи посилання завжди незамінні? Покажчики завжди можна використовувати замість них. Така нульова посилання дозволить вам використовувати посилання також тоді, коли ви не можете мати жодного об'єкта, на який слід посилатися. Не знаю, наскільки це брудно, але перш ніж подумати над цим, я зацікавився його законністю.
пеоро


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

2
"Однією з головних причин використовувати посилання замість вказівника - це звільнити вас від тягаря необхідності перевірити, чи не посилається він на дійсний об'єкт", ця відповідь за посиланням за замовчуванням звучить досить добре!
пеоро

Відповіді:


75

Посилання не є покажчиками.

8.3.2 / 1:

Посилання має ініціалізуватися для посилання на дійсний об'єкт або функцію. [Примітка. Зокрема, нульова посилання не може існувати у чітко визначеній програмі, оскільки єдиним способом створення такої посилання було б прив’язання її до "об'єкта", отриманого шляхом перенаправлення нульового вказівника, що спричиняє не визначену поведінку. Як описано в 9.6, посилання не може бути пов'язане безпосередньо з бітовим полем. ]

1,9 / 4:

Деякі інші операції описані в цьому Міжнародному стандарті як невизначені (наприклад, ефект відсилання нульового вказівника)

Як каже Йоаннес у видаленій відповіді, є певні сумніви, чи слід «перенаправлення нульового покажчика» категорично заявляти як невизначене поведінку. Але це не один із випадків, які викликають сумніви, оскільки нульовий покажчик, безумовно, не вказує на "дійсний об'єкт або функцію", і в комітеті зі стандартів немає бажання вводити нульові посилання.


Я видалив свою відповідь, оскільки зрозумів, що проста проблема з перенаправленням нульового вказівника та отримання значення, яке посилається на це, є різною річчю, ніж насправді обов'язкове посилання на нього, як ви згадуєте. Хоча, як кажуть, значення lvalu посилаються і на об'єкти чи функції (тому в цьому пункті насправді немає різниці у прив'язці посилань), ці дві речі все ще є окремими питаннями. Щодо простого акту перенаправлення,
Йоханнес Шауб - лібт

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

@ Стандартні стандарти в тегах [c ++] повинні бути високими. Моя відповідь звучала так, що обидва дії - це одне і те ж саме :) Хоча перенаправлення та отримання значення, яке ви не обходите, це означає, що "жоден об'єкт" не може бути зрозумілим, зберігання його у посиланні може уникнути обмеженого обсягу і раптом може вплинути. набагато більше коду.
Йоханнес Шауб - ліб

@Karl добре в C ++, "перенаправлення" не означає читати значення. Деякі люди думають, що "dereference" означає фактично отримати доступ або змінити збережене значення, але це неправда. Логіка полягає в тому, що C ++ говорить, що значення lvalue означає "об'єкт або функцію". Якщо це так, то питання полягає в тому, на що *pпосилається lvalue , коли pце нульовий покажчик. Наразі C ++ не має поняття порожнього значення, яке випуск 232 хотів запровадити.
Йоханнес Шауб - ліб

Виявлення відмінених нульових покажчиків у typeidроботах на основі синтаксису, а не на основі семантики. Тобто, якщо ви робите typeid(0, *(ostream*)0)ви робите не має невизначений поведінку - чи не bad_typeidгарантовано буде викинуто, навіть якщо ви передаєте іменує вираз в результаті з нульового покажчика семантичний. Але синтаксично на вершині, це не вивід, а вираз оператора комами.
Йоханнес Шауб - ліб

26

Відповідь залежить від точки зору:


Якщо судити за стандартом C ++, ви не можете отримати нульову посилання, оскільки ви спочатку отримуєте невизначене поведінку. Після першої випадковості невизначеної поведінки стандарт дозволяє щось статися. Отже, якщо ви пишете *(int*)0, у вас вже є невизначена поведінка такою, якою ви є, з мовної стандартної точки зору, дереферентування нульового вказівника. Решта програми не має значення, як тільки це вираження виконується, ви виходите з гри.


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

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

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

Коли компілятор бачить цю функцію, він не знає, чи вказівник є нульовим покажчиком чи ні. Таким чином, він просто генерує код, який перетворює будь-який вказівник у відповідну посилання. (Btw: Це noop, оскільки вказівники та посилання є точно таким же звіром у асемблері.) Тепер, якщо у вас є інший файл 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
Що саме "пінг-слон"?
Фарап

2
@Pharap У мене немає поняття, це була просто помилка. Але стандарт C ++ не байдуже, чи не з’являються рожеві або пінг-слони ;-)
cmaster - відновлюють

9

Якщо ваш намір полягав у тому, щоб знайти спосіб представити null при перерахуванні одиночних об'єктів, то погано зазначати (de) посилання на null (це C ++ 11, nullptr).

Чому б не оголосити статичний однотонний об'єкт, який представляє NULL у класі, як описано нижче, і не додати оператор "перехід на покажчик", який повертає nullptr?

Редагувати: виправлено декілька помилок та додано if-statement у 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

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.