Страницы

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

среда, 9 января 2019 г.

Вызов метода у нулевого указателя

Сегодня состоялся следующий спор с коллегами. Они утверждали, что в таком коде нет никаких проблем, и все будет работать везде одинаково:
#include
struct S{ int a; void foo(){ std::cout << "hello"; } };
int main(){ S *p = nullptr; p->foo(); //hello }
Мол к данным мы не обращаемся => В память по адресу 0 не лезем => Проблем нет. Я им с пеной у рта доказывал что если вызывать любой не статический метод у nullptr это сразу неопределенное поведение, и не важно что там в этом методе происходит.
Вопросы:
Кто прав? Где в стандарте об этом написано? Есть ли в стандарте что-то о том, как должен быть реализован this?


Ответ

В "классическом" С++ (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. При этом в языке С разыменование нулевого указателя строжайше запрещено.

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

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