Есть ли платформа или ситуация, когда разыменование (но не использование) нулевого указателя для создания нулевой ссылки будет вести себя плохо?
В настоящее время я использую библиотеку, которая использует код типа
T& being_a_bad_boy()
{
return *reinterpret_cast<T*>(0);
}
чтобы сделать ссылку на T без фактического наличия T. Это поведение undefined, специально отмеченное как неподдерживаемое стандартом, но оно не является неслыханным шаблоном.
Мне любопытно, есть ли какие-либо примеры или платформы или обычаи, которые показывают, что на практике это может вызвать проблемы. Может кто-нибудь предоставить некоторые?
Ответы
Ответ 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. Я даже слышал, что существуют архитектуры, где даже копирование вокруг ненулевого указателя на недопустимую память ловутся, возможно, есть и некоторые из них, которые делают различие между указателем и ссылкой.