bar(); // (a) f->baz();" />

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

c++ undefined-behavior language-lawyer standards-compliance null-pointer

13915 просмотра

2 ответа

311829 Репутация автора

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

#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) являются статическими?

Автор: GManNickG Источник Размещён: 18.03.2010 11:20

Ответы (2)


112 плюса

311829 Репутация автора

Решение

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


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

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

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

Lvalue относится к объекту или функции.

При разыменовании результатом является lvalue. Нулевой указатель не ссылается на объект, поэтому, когда мы используем lvalue, мы имеем неопределенное поведение. Проблема в том, что предыдущее предложение никогда не формулируется, так что значит «использовать» lvalue? Просто сгенерировать его вообще или использовать в более формальном смысле для преобразования lvalue в 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, неопределенность сохраняется. Пока будьте в безопасности: используйте строгую интерпретацию.

Автор: GManNickG Размещён: 18.03.2010 11:21

29 плюса

232906 Репутация автора

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

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

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

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

Автор: Mark Ransom Размещён: 29.09.2010 09:51
Вопросы из категории :
32x32