Подтвердить что ты не робот

Есть ли платформа или ситуация, когда разыменование (но не использование) нулевого указателя для создания нулевой ссылки будет вести себя плохо?

В настоящее время я использую библиотеку, которая использует код типа

T& being_a_bad_boy()
{
    return *reinterpret_cast<T*>(0);
}

чтобы сделать ссылку на T без фактического наличия T. Это поведение undefined, специально отмеченное как неподдерживаемое стандартом, но оно не является неслыханным шаблоном.

Мне любопытно, есть ли какие-либо примеры или платформы или обычаи, которые показывают, что на практике это может вызвать проблемы. Может кто-нибудь предоставить некоторые?

4b9b3361

Ответ 1

Классически компиляторы рассматривали поведение "undefined" как просто оправдание, чтобы не проверять различные типы ошибок и просто "пусть это произойдет в любом случае". Но современные компиляторы начинают использовать undefined поведение для оптимизации оптимизации.

Рассмотрим этот код:

int table[5];
bool does_table_contain(int v)
{
    for (int i = 0; i <= 5; i++) {
        if (table[i] == v) return true;
    }
    return false;
}

Классические компиляторы не заметили бы, что ваш предел цикла был написан неправильно и что последняя итерация читается с конца массива. Он просто попытался бы прочитать конец массива в любом случае и вернуть true, если бы значение, имевшее значение после конца массива, соответствовало.

Послеклассический компилятор, с другой стороны, может выполнить следующий анализ:

  • Первые пять раз через цикл функция может возвращать true.
  • Когда i = 5, код выполняет поведение undefined. Поэтому случай i = 5 можно рассматривать как недостижимый.
  • Случай i = 6 (цикл завершается до конца) также недоступен, потому что для этого вам сначала нужно сделать i = 5, который мы уже показали, недоступен.
  • Следовательно, все доступные пути кода возвращаются true.

Затем компилятор упростит эту функцию до

bool does_table_contain(int v)
{
    return true;
}

Другим способом взглянуть на эту оптимизацию является то, что компилятор мысленно развернул цикл:

bool does_table_contain(int v)
{
    if (table[0] == v) return true;
    if (table[1] == v) return true;
    if (table[2] == v) return true;
    if (table[3] == v) return true;
    if (table[4] == v) return true;
    if (table[5] == v) return true;
    return false;
}

И затем он понял, что оценка table[5] равна undefined, поэтому все, что находится за пределами этой точки, недостижимо:

bool does_table_contain(int v)
{
    if (table[0] == v) return true;
    if (table[1] == v) return true;
    if (table[2] == v) return true;
    if (table[3] == v) return true;
    if (table[4] == v) return true;
    /* unreachable due to undefined behavior */
}

а затем обратите внимание, что все доступные пути кода возвращаются true.

Компилятор, который использует поведение undefined для управления оптимизацией, будет видеть, что каждый путь кода через функцию being_a_bad_boy вызывает поведение undefined, и поэтому функция being_a_bad_boy может быть сведена к

T& being_a_bad_boy()
{
    /* unreachable due to undefined behavior */
}

Этот анализ затем может обратно распространяться на всех вызывающих абонентов being_a_bad_boy:

void playing_with_fire(bool match_lit, T& t)
{
    kindle(match_lit ? being_a_bad_boy() : t);
} 

Поскольку мы знаем, что being_a_bad_boy недоступен из-за поведения undefined, компилятор может заключить, что match_lit никогда не должен быть true, в результате чего

void playing_with_fire(bool match_lit, T& t)
{
    kindle(t);
} 

И теперь все ловят огонь, независимо от того, горит ли матч.

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

Ответ 2

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

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

T &bad = being_a_bad_boy();
if (&bad == NULL)  // this could be optimized away!

Изменить: я буду бесстыдно украсть из комментария @mcmcc и указать, что эта общая идиома, скорее всего, потерпит крах, потому что она использует недопустимую ссылку. Согласно Закону Мерфи, это будет в худшем возможном моменте, и, конечно же, никогда во время тестирования.

T bad2 = being_a_bad_boy();

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

T &bad3 = being_a_bad_boy();
bad3.do_something();

T::do_something()
{
    use_a_member_of_T();
}

T::use_a_member_of_T()
{
    member = get_unrelated_value(); // crash occurs here, leaving you wondering what happened in get_unrelated_value
}

Ответ 3

Я ожидал бы, что на большинстве платформ компилятор преобразует все ссылки в указатели. Если это предположение верно, то это будет идентично простому прохождению вокруг указателя NULL, что отлично до тех пор, пока вы его никогда не используете. Возникает вопрос, существуют ли какие-либо компиляторы, которые обрабатывают ссылки каким-то образом, кроме как просто конвертируют их в указатели. Я не знаю таких компиляторов, но я полагаю, что они существуют.

Ответ 4

Используйте Шаблон NullObject.

class Null_T : public T
{
public:
    // implement virtual functions to do whatever
    // you'd expect in the null situation
};

T& doing_the_right_thing()
{
    static Null_T null;
    return null;
}

Ответ 5

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

Если ваш код может привести к недопустимому объекту, то либо он возвращает указатель (желательно, умный указатель, но и другое обсуждение), используйте шаблон нулевого объекта, упомянутый выше (boost:: optional может быть полезен здесь) или выбросить исключение.

Ответ 6

Я не знаю, достаточно ли для вас проблем, или достаточно близко к вашему "случаю использования", это сбой для меня в gcc (на x86_64):

int main( )
{
    volatile int* i = 0;
    *i;
}

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

Еще одно не очень очевидное плохое может случиться, когда вы вызываете виртуальную функцию на нулевом указателе (из-за обычно выполняемого через vptr на vtable), и, как таковое, это относится к (в стандартном С++ не существующем) null Справка.

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