#cpp #неопределенное_поведение
Сегодня состоялся следующий спор с коллегами. Они утверждали, что в таком коде нет никаких проблем, и все будет работать везде одинаково: #includestruct S{ int a; void foo(){ std::cout << "hello"; } }; int main(){ S *p = nullptr; p->foo(); //hello } Мол к данным мы не обращаемся => В память по адресу 0 не лезем => Проблем нет. Я им с пеной у рта доказывал что если вызывать любой не статический метод у nullptr это сразу неопределенное поведение, и не важно что там в этом методе происходит. Вопросы: Кто прав? Где в стандарте об этом написано? Есть ли в стандарте что-то о том, как должен быть реализован this?
Ответы
Ответ 1
В "классическом" С++ (C++98) ситуация однозначная - разыменование нулевого указателя приводит к неопределенному поведению. Соответственно вызов нестатического метода объекта через нулевой указатель приводит к неопределенному поведению. Не имеет никакого значения, выполняет ли этот метод доступ к членам класса или не выполняет. Такова позиция спецификации языка. С этой точки зрения вы совершенно правы, а аргументы ваших оппонентов на тему "все везде будет работать" - не более чем следствие "уличного образования" из разряда "смотрю в книгу ассемблер, вижу фигу". В то же время уже довольно давно делаются попытки формирования более гибкой/тонкой спецификации в этом вопросе. В частности DR#232: Is indirection through a null pointer undefined behavior? Однако работа в этом направлении перманентно зависла в состоянии drafting с 2005 года. Честно говоря, создается впечатление, что какого-то внятного толкования текста нынешнего стандарта на эту тему никто дать не может, возможно именно потому, что тема до сих пор является "подвешенной". Как вы сами понимаете, стандарт не будет заводить отдельную спецификацию на именно ваш частный случай. А как только мы переходим к более общему случаю, то сразу возникают такие ситуации, как преобразование указателя this при вызове метода в условиях [множественного] наследования. struct A { int a; }; struct B { int b; void foo() { // К данным мы не обращаемся // Но чему здесь равно `this`??? if (this == nullptr) ; // ??? } }; struct C : A, B { }; int main() { C *c = nullptr; c->foo(); } Не ясно, должен ли компилятор при преобразованиях указателя this в процессе вызова метода базового класса придерживаться правила "null преобразовывается в null"? Вот именно из-за таких тонкостей изначально было принято решение запретить вызовы нестатических методов через нулевой указатель. Что же будет (и есть) сейчас и каковы намерения авторов языка - надо ждать и разбираться. Обратите внимание, кстати, что 8.5.1.2/4 требует, чтобы скрытый параметр this при вызове метода класса инициализировался при помощи explicit type conversion. То есть в вышеприведенном примере с множественным наследованием в вызове c->foo() указатель this типа B должен инициализироваться как (B *) c. Такое преобразование работает по правилу null-в-null и результат (B *) c тоже будет нулевым указателем. Однако в GCC и Clang this внутри foo во время вызова будет иметь значение 0x4. То есть эти компиляторы не выполнили требований 8.5.1.2/4. Это сразу говорит о том, что GCC и Clang по-прежнему трактуют такой вызов как неопределенное поведение. P.S. При этом в языке С разыменование нулевого указателя строжайше запрещено.Ответ 2
Код struct S{ int a; void foo(){ std::cout << "hello"; } }; идентичен такому void foo(s * this){ std::cout << "hello"; } Таким образом вызов S *p = nullptr; p->foo(); //hello эквивалентен такому foo(nullptr) Для проверки просто загляните в окно CPU. Оба вызова сгенерируют один и тот же код mov eax, 0 push eax call foo т.е. пока Вы не обращаетесь к this проблем нет вообще. Обращение к this произойдет в двух случаях Вы обратитесь к конкретному полю (может даже из другого метода) Вы вызовете виртуальную функцию, и программа полезет читать VMT по нулевым указателям А в статические функции указатель this не передается вообще
Комментариев нет:
Отправить комментарий