Когда вызов функции-члена для нулевого экземпляра приводит к неопределенному поведению? - PullRequest
114 голосов
/ 19 марта 2010

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

#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 имеет значение null, программа вводит неопределенное поведение, поскольку разыменование null всегда называется неопределенным поведением.

Приводит ли (a) к неопределенному поведению? А что, если обе функции (и x) являются статическими?

Ответы [ 2 ]

111 голосов
/ 19 марта 2010

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


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

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

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

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

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

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

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

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

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

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

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


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

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

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

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

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

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


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

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

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

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

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

29 голосов
/ 30 сентября 2010

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

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

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

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

...