Рассмотрим следующий код:
#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
является нулевым, программа переходит в неопределенное поведение, поскольку разыменование нулевого значения всегда считается неопределенным поведением.
Приводит ли (a)
к неопределенному поведению? Что если обе функции (и x
) статичны?
x
тоже стало статичным. :)Ответы:
Оба
(a)
и(b)
приводят к неопределенному поведению. Вызов функции-члена через нулевой указатель всегда является неопределенным. Если функция статическая, она также технически не определена, но есть некоторые споры.Прежде всего необходимо понять, почему разыменование нулевого указателя является неопределенным. В C ++ 03 здесь действительно есть некоторая двусмысленность.
Хотя «разыменование нулевого указателя приводит к неопределенному поведению» упоминается в примечаниях как в §1.9 / 4, так и в §8.3.2 / 4, это никогда не указывается явно. (Примечания не являются нормативными.)
Однако можно попытаться вывести его из §3.10 / 2:
При разыменовании результатом является lvalue. Нулевой указатель не относится к объекту, поэтому, когда мы используем lvalue, мы имеем неопределенное поведение. Проблема в том, что предыдущее предложение никогда не формулируется, так что же значит «использовать» lvalue? Просто даже сгенерировать его вообще или использовать в более формальном смысле для преобразования lvalue-to-rvalue?
Тем не менее, его определенно нельзя преобразовать в rvalue (§4.1 / 1):
Здесь определенно неопределенное поведение.
Неопределенность возникает из-за того, является ли неопределенное поведение уважительным, но не использует ли значение из недопустимого указателя (то есть получить lvalue, но не преобразовать его в rvalue). Если нет, то
int *i = 0; *i; &(*i);
это хорошо определено. Это активный вопрос .Итак, у нас есть строгое представление «разыменовать нулевой указатель, получить неопределенное поведение» и слабое представление «использовать разыменованный нулевой указатель, получить неопределенное поведение».
Теперь рассмотрим вопрос.
Да,
(a)
приводит к неопределенному поведению. Фактически, еслиthis
имеет значение null, то независимо от содержимого функции результат не определен.Это следует из §5.2.5 / 3:
*(E1)
приведет к неопределенному поведению со строгой интерпретацией и.E2
преобразует его в rvalue, делая неопределенное поведение для слабой интерпретации.Отсюда также следует, что это неопределенное поведение непосредственно из (§9.3.1 / 1):
Что касается статических функций, разница между строгой и слабой интерпретацией. Строго говоря, это не определено:
То есть он оценивается так же, как если бы он был нестатическим, и мы снова разыменовываем нулевой указатель с помощью
(*(E1)).E2
.Однако, поскольку
E1
он не используется в вызове статической функции-члена, если мы используем слабую интерпретацию, вызов будет четко определен.*(E1)
приводит к lvalue, статическая функция разрешается,*(E1)
отбрасывается и функция вызывается. Преобразование lvalue-to-rvalue отсутствует, поэтому неопределенное поведение отсутствует.В C ++ 0x, начиная с n3126, неоднозначность сохраняется. А пока будьте осторожны: используйте строгую интерпретацию.
источник
*p
не является ошибкой, если значениеp
null, если lvalue не преобразовано в rvalue». Однако это основано на концепции «пустого lvalue», которая является частью предложенного решения по дефекту 232 CWG , но не была принята. Таким образом, с языком как C ++ 03, так и C ++ 0x разыменование нулевого указателя все еще не определено, даже если нет преобразования lvalue-to-rvalue.p
был аппаратный адрес, который запускал бы какое-то действие при чтении, но не был объявленvolatile
, оператор*p;
не потребовался бы, но ему было бы разрешено фактически прочитать этот адрес; заявлению&(*p);
, однако, это будет запрещено. Если бы*p
былиvolatile
, то чтение потребовалось бы. В любом случае, если указатель недействителен, я не вижу, как первый оператор не будет неопределенным поведением, но я также не понимаю, почему второй оператор будет.Очевидно, что undefined означает, что он не определен , но иногда это можно предсказать. На информацию, которую я собираюсь предоставить, никогда нельзя полагаться на рабочий код, поскольку это, конечно, не гарантируется, но может пригодиться при отладке.
Вы можете подумать, что вызов функции для указателя объекта приведет к разыменованию указателя и вызовет UB. На практике, если функция не виртуальная, компилятор преобразует ее в простой вызов функции, передав указатель в качестве первого параметра this , минуя разыменование и создавая бомбу замедленного действия для вызываемой функции-члена. Если функция-член не ссылается ни на какие переменные-члены или виртуальные функции, она может успешно завершиться без ошибок. Помните, что успех относится к вселенной undefined!
Функция Microsoft MFC GetSafeHwnd фактически полагается на это поведение. Я не знаю, что они курили.
Если вы вызываете виртуальную функцию, необходимо разыменовать указатель, чтобы перейти к vtable, и вы наверняка получите UB (возможно, сбой, но помните, что нет никаких гарантий).
источник
GetSafeHwnd
, возможно, с тех пор они его улучшили. И не забывайте, что у них есть инсайдерские знания о работе компилятора!