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

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

#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) статичны?

115
задан pbn 30 September 2015 в 18:44
поделиться

1 ответ

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


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

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

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

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

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

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

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

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

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

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

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


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

Это следует из §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-to-rvalue нет, поэтому нет неопределенного поведения.

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

112
ответ дан 24 November 2019 в 02:27
поделиться
Другие вопросы по тегам:

Похожие вопросы: