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

StackOverflow https://stackoverflow.com/questions/2474018

Вопрос

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

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

Это было полезно?

Решение

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


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

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

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

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

При разыменовании результатом является значение 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 преобразует его в rvalue, делая его неопределенным поведением для слабой интерпретации.

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

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


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

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

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

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

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

Другие советы

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

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top