Страницы

Поиск по вопросам

вторник, 28 января 2020 г.

Разыменование nullptr

#cpp #указатели


Что происходит при разыменовании nullptr?
    


Ответы

Ответ 1



Разыменование nullptr есть undefined behaviour. Это значит, что компилятор имеет право предполагать, что эта ситуация никогда не случится, и обрабатывать её как ему вздумается. Отсюда следует, что если в вашей программе есть разыменование nullptr, то компилятор имеет право произвести любой код, никаких обязательств перед вами или гарантий нет. В лучшем случае ваша программа просто крешнется. В худшем — будет вести себя необъяснимым странным образом. Вот пример, упрощённый из реального критического бага в ядре Линукса [GNU/Linux, как говорит RMS]. (Украдено отсюда.) void contains_null_check(int *P) { int dead = *P; if (P == 0) return; *P = 4; } В этом примере кажется, что код проверяет указатель на nullptr. Если оптимизатор сначала проведёт удаление мёртвого кода, а затем удаление бессмысленных проверок, наш код превратится вот во что: // после удаления мёртвого кода void contains_null_check_stage1(int *P) { //int dead = *P; // неиспользуемая переменная, убрано оптимизатором if (P == 0) return; *P = 4; } и затем: // после удаления бессмысленных проверок void contains_null_check_stage2(int *P) { if (P == 0) // проверка на null имеет смысл, оставляем. return; *P = 4; } Но если оптимизатор реализован по-другому, и выполняет оптимизации в другом порядке, мы получаем следующее: // после удаления бессмысленных проверок void contains_null_check_stage1(int *P) { int dead = *P; if (false) // в этой точке P был разыменован, он не может быть nullptr return; *P = 4; } и затем: // после удаления мёртвого кода void contains_null_check_stage2(int *P) { //int dead = *P; //if (false) // return; *P = 4; } Что произошло на первом этапе? Для компилятора разыменование nullptr есть undefined behaviour, он имеет право предполагать, что этого не происходит. Значит, видя строчку int dead = *P;, он имеет право предполагать, что P не nullptr. Поэтому проверку на nullptr он может выкинуть как бессмысленную. Для многих нормальных программистов удаление проверки на nullptr из этой функции выглядит очень странно (и они наверное даже отправят баг разработчикам компилятора). Тем не менее, оба варианта компиляции соответствуют стандарту на 100%, и каждая из приведённых оптимизаций очень важна для производительности. Несмотря на то, что этот пример кажется слишком простым и надуманным, подобные вещи часто случаются неявно в результате подстановки inline-функций: подстановка даёт возможность большей оптимизации. Это означает, что если оптимизатор решает включить функцию в другую функцию, большое количество локальных оптимизаций тут же включаются в игру, и это может поменять поведение кода. Это тоже разрешено по стандарту, и очень важно для хорошей производительности скомпилированного кода. Мораль этой истории — компилятор C больше не является «высокоуровневым ассемблером», и выполняемый код может быть очень далёк от буквального, построчного выполнения того, что вы написали.

Ответ 2



nullptr является литералом, имеющим тип std::nullptr_t. Для данного типа операция разыменования отсутствует, поэтому в исходной формулировке: Что происходит при разыменовании nullptr? ответом будет: ошибка компиляции, с выводом соответствующего сообщения: error: indirection requires pointer operand ('nullptr_t' invalid) Если же речь о разыменовании нативного указателя на некоторый тип T, которому предварительно было присвоено значение nullptr: T* p = nullptr; *p; то тут происходит неявное преобразование из типа std::nullptr_t в тип T* с последующим разыменованием. И вот такое действие уже приводит к неопределенному поведению.

Комментариев нет:

Отправить комментарий