Раньше я считал, что программа сразу же упадет при разыменовании нулевого указателя.
Но фрагмент кода ниже работает хорошо. Какой сюрприз!
#include <iostream>
#include <vector>
struct foo
{
void test()
{
std::cout << "This is displayed\n";
}
};
int main()
{
foo *f = nullptr;
f->test();
}
Распространенное заблуждение, но это была бы функция безопасности, которой нет в C++ по умолчанию.
В C++ компилируемый код не обязательно является правильной программой и не обязательно работает (правильно). Разыменование nullptr не разрешено C++ (виртуальная машина C++), и C++ может генерировать любой код, который захочет. (проблемы обычно проявляются, когда вы выполняете оптимизированную сборку, хотя ваш пример, похоже, тоже является исключением: godbolt.org/z/ebPqbKere). Вы можете видеть, что весь вызов функции оптимизирован, поскольку «это» не требуется.
Посмотрите это: CppCon 2017: Джон Регер «Неопределённое поведение в 2017 году (часть 1 из 2)».
Когда вы переходите красный свет светофора, вас не сразу сбивает машина. То же самое
Пожалуйста, проведите надлежащее исследование, прежде чем задавать какой-либо вопрос, как рекомендовано кнопкой «понижение» в вопросе сообщения и другими рекомендациями SO.
См. en.cppreference.com/w/cpp/language/ub
Раньше я считал, что программа сразу же упадет при разыменовании нулевого указателя. Это не гарантия C++. C++ не определяет, что происходит при разыменовании нулевого указателя. Вот почему разыменование нулевого указателя — это классическое неопределенное поведение. Сбой из-за этого происходит из-за вашей платформы. Не все платформы потерпят крах из-за этого. (Ваш код дает сбой на моей машине.)





Разыменование nullptr — это неопределенное поведение. Таким образом, он может выйти из строя или нет. Компилятор может просто пропустить/оптимизировать строку с помощью этой операции, поскольку со стороны пользователя гарантируется, что nullptr никогда не будет разыменовано.
Почему компилятор предпочитает игнорировать процесс разыменования? Просто потому, что для структуры нет переменных-членов?
Пытаться объяснить, почему компилятор делает что-то, если у вас неопределенное поведение, не имеет смысла.
@John Все оптимизации основаны на некоторых предположениях. Например, вам нужно предположить, что вектор отсортирован для выполнения двоичного поиска. Итак, давайте возьмем эти предположения куда-нибудь. Давайте договоримся, что пользователи будут следовать некоторым правилам и делать оттуда предположения. Давайте договоримся, что пользователь никогда не будет разыменовывать nullptr. Тогда, если все пространство возможных путей может пройти выполнение, мы можем предположить, что пути, ведущие к *nullptr, невозможны (пользователь гарантирует это). Итак, давайте удалим их из кода. Отсутствие переменных-членов не имеет к этому никакого отношения. Речь идет о том, как работает оптимизация в целом.
Другими словами, компилятор предполагает, что он знает, что вы делаете (чтобы сделать его жизнь намного проще);)
Почему компилятор предпочитает игнорировать процесс разыменования? Это не так. Он анализирует конструкции кода (на основе правил, предположений и т. д.) и, если этот анализ определяет, что некоторый набор конструкций (операции с конкретными переменными, сложные выражения, [встроенные] вызовы функций и т. д. и т. п.) не оказывают видимого влияния на выходные данные программы, их можно опустить. Это так же верно для конструкций с неопределенным поведением, как и для любых других конструкций (с той лишь разницей, что с неопределенным поведением разные компиляторы могут выдавать разные наблюдаемые результаты).
Ваш код на самом деле не смотрит на указатель this, который передается в качестве скрытого первого аргумента функции-члена. Вот как оно выжило. Если вы добавите что-то, что будет использовать this, например, создадите функцию virtual, произойдет сбой, как вы и ожидали.
https://godbolt.org/z/YfxcdqdPj
Конечно, неопределенное поведение не определено, в этом случае компилятору разрешено делать абсолютно все, поэтому логические рассуждения не помогут вам.
Если вы включите оптимизацию, компилятор также может просто вставить туда инструкцию ud2 (явный сбой).
https://godbolt.org/z/YWfMefYoG
В вашем первом пункте ".... произойдет сбой, как вы и ожидали". это неверно. Более точным утверждением было бы: «...он может выйти из строя, как вы и ожидали». На практике, безусловно, можно найти примеры кода, подобного тому, что вы описываете, который НЕ ВЫЗЫВАЕТ сбой при использовании некоторых или всех компиляторов (или параметров сборки), которые вы использовали.
Неопределенное поведение по сути означает «все может случиться». С этой точки зрения вы больше не сможете рассуждать. Единственный способ узнать, что на самом деле происходит (т. е. что сделал компилятор), — это посмотреть на сгенерированный ассемблерный код.