*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***

понедельник, 19 июля 2010 г.

Нулевые ссылки в С++

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

...
class A { virtual void f() {} };
class B {};

int main() {
  A a;
  try {
    B& b = dynamic_cast<B&>(a);
    if (&b == 0) {
      // ...
    }
  } catch (...) {
    std::cout << "Got it!" << std::endl;
  }
  return 0;
}

Я слегка выпал в осадок от уведенного, а в частности, от строки "if (&b == 0) {". До сего времени я пребывал в осознании факта, что ссылка в С++ либо существует и указывает на реальный объект, либо ее нет вообще. И если тут приведение типа к "B&" не срабатывает, то будет исключение, и управление все равно улетит в другое место, и проверять как-либо "b" бессмысленно.

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

Ну да ладно. Оставим это на откуп странным компиляторам на AIX и SUN, и людям, использующим исключения, но почему-то компилирующие с принудительным их выключением в компиляторе.

Меня заинтересовал другой вопрос: как вообще ссылка может существовать отдельно от объекта. Оказывается, может:

int& a = *(int*)0;
int main() { a = 123; }

Данный код прекрасно компилируется Студией (2010) и компилятором SUN (этот хоть предупреждение выдает), и также прекрасно падается при запуске по понятой причине.

Вы получили ссылку в качестве параметра и думаете, что она лучше чем указатель, так как ее не надо проверять на null? Зря!

10 комментариев:

  1. >Зря!
    Тут тонкий момент есть. Если в функцию передают указатель - его надо проверять на валидность внутри функции, так же как и остальные параметры.

    Если же в функцию передают ссылку - то "вина за некорректную работу" лежит уже на том, кто создал такой "объект", а не на этой функции.

    Более того, компилятор имеет право заоптимизировать выражение вида (&b==0), так как никаким валидным способом создать такой объект нельзя. Это ещё одна причина не проверять ссылки на не-null.

    ОтветитьУдалить
  2. Конечно, я не имел ввиду такую ручную проверку. Просто получается, что работа по ссылке может привести к тем же проблемам, как и работа с указателем.

    ОтветитьУдалить
  3. Да, конечно. Ссылка - это такой автоматически разыменованный указатель, не больше.

    ОтветитьУдалить
  4. Согласно стандарту, разыменование нулевого указателя есть неопределённое поведение. Поэтому дальше может произойти всё что угодно. В принципе, оптимизатор может просто выкинуть доступ по нулевому указателю из генерируемого кода. Некоторые компиляторы (Clang, например), выдают в ответ на разыменование нулевого указателя сообщение об ошибке.

    Кстати, ссылка не обязательно явяется автоматически разыменованным указателем (я тоже раньше так думал). На самом деле, это оставляется на усмотрение компилятора. Например, при объявлении
    int a = 0;
    int& b = a;
    присваивание
    b = 1;
    может быть просто заменено компилятором на
    a = 1;

    ОтветитьУдалить
  5. Небольшое замечание не по теме: интересно, что согласно стандарту использование dynamic_cast при работе со ссылками должно выбрасывать исключение bad_cast в любом случае. Поэтому конструкция "if (&b == 0)" в общем-то бессмысленна для компиляторов, которые следуют стандарту, потому что в случае ошибки в строке с dynamic_cast'ом произойдет простой terminate или что-то в этом духе.

    ОтветитьУдалить
  6. Вопрос про отключение исключений вообще более широк. Например, в любой книге по С++ можно увидеть что-то типа "результат new не надо проверять на NULL". И что же? Отключаем исключения - и уже надо?

    ОтветитьУдалить
  7. Кстати, нулевая ссылка может получиться и куда более прозаическим образом:

    A *pa = 0;
    A &a = *pa;

    Вторая строчка не является разыменованием, это просто объявление ссылки - псевдоним *pa. Т.е. тут все по Стандарту.

    В жизни это может случиться так:

    void foo(A *pa) {
    ...
    bar(*pa);
    }

    void bar(A &a) {
    ...
    }

    void wilma() {
    foo(NULL);
    }

    ОтветитьУдалить
  8. >Вторая строчка не является разыменованием
    Является, является.

    ОтветитьУдалить
  9. Из Стандарта:
    "in particular, a null reference cannot exist in a well-defined program, because the only way to create such a reference would be to bind it to the “object” obtained by dereferencing a null pointer, which causes undefined behavior."

    ОтветитьУдалить