Существует ли платформа или ситуация, когда разыменование (но не использование) нулевого указателя для создания нулевой ссылки будет вести себя плохо? - PullRequest
25 голосов
/ 21 февраля 2012

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

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

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

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

Ответы [ 6 ]

84 голосов
/ 26 февраля 2012

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

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

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, код выполняет неопределенное поведение. Поэтому дело 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] не определена, поэтому все, что прошло после этой точки, недостижимо:

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.

Компилятор, который использует неопределенное поведение для управления оптимизацией, увидит, что каждый путь кода через функцию being_a_bad_boy вызывает неопределенное поведение, и, следовательно, функцию 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 недоступен из-за неопределенного поведения, компилятор может заключить, что match_lit никогда не должен быть true, что приводит к

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

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

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

19 голосов
/ 21 февраля 2012

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

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

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

Edit: я собираюсь бесстыдно украсть из комментария @ 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
}
1 голос
/ 08 марта 2012

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

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

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

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

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

1 голос
/ 28 февраля 2012

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

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

1 голос
/ 28 февраля 2012

Используйте шаблон 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;
}
1 голос
/ 21 февраля 2012

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...