Когда вызов функции-члена в экземпляре null приводит к поведению undefined?

Рассмотрим следующий код:

#include <iostream>

struct foo
{
    // (a):
    void bar() { std::cout << "gman was here" << std::endl; }

    // (b):
    void baz() { x = 5; }

    int x;
};

int main()
{
    foo* f = 0;

    f->bar(); // (a)
    f->baz(); // (b)
}

Мы ожидаем сбой (b), потому что для нулевого указателя нет соответствующего члена x. На практике (a) не сбой, потому что указатель this никогда не используется.

Поскольку (b) разделяет указатель this ((*this).x = 5;) и this равен нулю, программа вводит поведение undefined, так как разыменование нуля всегда называется undefined.

Выполняет ли (a) поведение undefined? Как насчет того, что обе функции (и x) являются статическими?

Ответ 1

Оба (a) и (b) приводят к поведению undefined. Всегда undefined поведение вызывает функцию-член через нулевой указатель. Если функция статична, она технически undefined, но есть некоторые споры.


Первое, что нужно понять, - это почему поведение undefined для разыменования нулевого указателя. В С++ 03 здесь есть немного двусмысленности.

Хотя "разыменование нулевого указателя приводит к поведению undefined" упоминается в примечаниях как в §1.9/4, так и в § 8.3.2/4, оно никогда явно не указано. (Примечания не являются нормативными.)

Однако можно попытаться вывести его из § 3.10/2:

Значение l относится к объекту или функции.

При разыменовании результат равен lvalue. Нулевой указатель не относится к объекту, поэтому, когда мы используем lvalue, мы имеем поведение undefined. Проблема в том, что предыдущее предложение никогда не указано, так что значит "использовать" значение lvalue? Просто даже создайте его вообще или используйте его в более формальном смысле преобразования lvalue-to-rvalue?

Несмотря на это, он определенно не может быть преобразован в rvalue (§4.1/1):

Если объект, к которому относится lvalue, не является объектом типа T и не является объектом типа, производного от T, или если объект не инициализирован, программа, которая требует этого преобразования, имеет поведение undefined.

Здесь это определенно поведение undefined.

Неоднозначность исходит из того, относится ли поведение undefined к уважению, но не использует значение из недопустимого указателя (то есть получает значение lvalue, но не преобразует его в rvalue). Если нет, то int *i = 0; *i; &(*i); четко определен. Это активная проблема .

Таким образом, у нас есть строгий "разыменовать нулевой указатель, получить представление" w90 > поведение "и" слабый "использовать разыменованный нулевой указатель, получить представление" w90 > поведение ".

Теперь рассмотрим вопрос.


Да, (a) приводит к поведению undefined. Фактически, если this равно null, то независимо от содержимого функции результат равен undefined.

Это следует из п. 5.2.5/3:

Если E1 имеет тип "указатель на класс X", то выражение E1->E2 преобразуется в эквивалентную форму (*(E1)).E2;

*(E1) приведет к поведению undefined со строгой интерпретацией, а .E2 преобразует его в значение r, что делает его undefined для слабой интерпретации.

Отсюда также следует, что поведение undefined непосредственно из (§9.3.1/1):

Если нестатическая функция-член класса X вызывается для объекта, который не относится к типу X или типа, полученного из X, поведение undefined.


Со статическими функциями строгая и слабая интерпретация делает разницу. Строго говоря, это undefined:

Статический член может ссылаться на использование синтаксиса доступа к члену класса, и в этом случае оценивается объектное выражение.

То есть, он оценивается так же, как если бы он был нестатическим, и мы снова разыменовали нулевой указатель с помощью (*(E1)).E2.

Однако, поскольку E1 не используется в статическом вызове функции-члена, если мы используем слабую интерпретацию, вызов корректно определен. *(E1) приводит к lvalue, статическая функция разрешена, *(E1) отбрасывается, и функция вызывается. Нет преобразования lvalue-to-rvalue, поэтому не существует поведения undefined.

В С++ 0x, начиная с n3126, остается неопределенность. Пока что будьте в безопасности: используйте строгую интерпретацию.

Ответ 2

Очевидно, что undefined означает не определено, но иногда это может быть предсказуемо. Информация, которую я собираюсь предоставить, никогда не должна зависеть от рабочего кода, поскольку она, безусловно, не гарантируется, но может пригодиться при отладке.

Вы можете подумать, что вызов функции на указателе объекта приведет к разыменованию указателя и вызову UB. На практике, если функция не является виртуальной, компилятор преобразует ее в обычный вызов функции, передавая указатель в качестве первого параметра, минуя разыменование и создавая бомбу замещения для вызываемой функции-члена. Если функция-член не ссылается ни на какие-либо переменные-члены, ни на виртуальные функции, она может быть успешной без ошибок. Помните, что последующее попадает во вселенную из "undefined"!

Функция Microsoft MFC GetSafeHwnd действительно полагается на это поведение. Я не знаю, что они курили.

Если вы вызываете виртуальную функцию, указатель должен быть разыменован, чтобы перейти на vtable, и, конечно же, вы получите UB (возможно, сбой, но помните, что нет никаких гарантий).