Это неопределенное поведение в C ++, вызывающее функцию из висячего указателя?

В SO возник вопрос: «Почему это работает?», Когда указатель стал зависать. Ответы были, что это UB, а значит может работать или нет.

В учебнике я узнал, что:

#include <iostream>

struct Foo
{
    int member;
    void function() { std::cout << "hello";}

};

int main()
{
    Foo* fooObj = nullptr;
    fooObj->member = 5; // This will cause a read access violation but...
    fooObj->function(); // Because this doesn't refer to any memory specific to
                        // the Foo object, and doesn't touch any of its members
                        // It will work.
}

Было бы это эквивалентом:

static void function(Foo* fooObj) // Foo* essentially being the "this" pointer
{
    std::cout << "Hello";
    // Foo pointer, even though dangling or null, isn't touched. And so should 
    // run fine.
}

Я ошибаюсь в этом? Это UB, хотя, как я объяснил, просто вызывает функцию и не обращается к недопустимому указателю Foo?


person Zebrafish    schedule 10.03.2018    source источник
comment
Это тема для дискуссий. Примеры возможных дубликатов: stackoverflow.com/a/28483256/560648 stackoverflow.com/q/3498444/560648 stackoverflow.com/q/5248877/560648 Эти вопросы в основном касаются доступа к статическим членам, но доступ к членам no - это, в конечном счете, один и тот же вопрос.   -  person Lightness Races in Orbit    schedule 10.03.2018
comment
@ Lightness Races на орбите Должен ли я тогда предположить, что никто не знает настоящего ответа, но я не должен играть с огнем?   -  person Zebrafish    schedule 10.03.2018
comment
Нет настоящего ответа, он не определен, вы не можете пытаться связать конкретное поведение с чем-то, что является неопределенным.   -  person Hatted Rooster    schedule 10.03.2018
comment
@Zebra: Лично я думаю, что вы можете смело считать это UB, но это было бы разумным запасным вариантом, да   -  person Lightness Races in Orbit    schedule 10.03.2018
comment
@SombreroChicken: есть ли у него UB или нет (якобы) не совсем ясно; в этом-то и дело   -  person Lightness Races in Orbit    schedule 10.03.2018
comment
Не определено, является ли оно неопределенным   -  person Daniel    schedule 10.03.2018
comment
@LightnessRacesinOrbit: Вы согласны с тем, что попытка создания нулевой ссылки - это UB в момент формирования ссылки, а не позже, когда она используется, верно?   -  person Ben Voigt    schedule 10.03.2018
comment
@LightnessRacesinOrbit Давай спросим Бьярна?   -  person Hatted Rooster    schedule 10.03.2018
comment
@BenVoigt: Я всегда так считал, да, но я встречал удивительное количество разногласий по этому поводу.   -  person Lightness Races in Orbit    schedule 10.03.2018
comment
@LightnessRacesinOrbit: Хотя я склонен согласиться с тем, что ни одно из стандартных правил не запрещает формирование плохой ссылки lvalue (UB появляется во время преобразования lvalue в rvalue), нельзя отрицать, что сам Стандарт прямо заявляет, что нулевые ссылки не могут существовать и пытается вызвать одну это темная магия UB.   -  person Ben Voigt    schedule 10.03.2018
comment
@LightnessRacesinOrbit Есть действительно разногласия? Я даже могу найти цепочку дубликатов, говорящих об одном и том же. Начиная с этого: stackoverflow.com/questions/11320822/   -  person llllllllll    schedule 10.03.2018
comment
@liliscent: Посмотрите мой первый комментарий к этой ветке   -  person Lightness Races in Orbit    schedule 10.03.2018
comment
@BenVoigt: Нормативно нигде об этом не говорится. Даже если бы это было так, это не ответило бы на этот вопрос.   -  person Lightness Races in Orbit    schedule 10.03.2018
comment
За исключением случаев, когда стандарт говорит, что p- ›function () эквивалентен (* (p)). Function (), тогда это разыменование указателя, и это UB .... Я думаю.   -  person Zebrafish    schedule 10.03.2018


Ответы (3)


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

Для нестатического случая это просто доказать, используя правило, найденное в [class.mfct.non-static]:

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

Обратите внимание, что не учитывается, обращается ли нестатическая функция-член к *this. Просто требуется, чтобы объект имел правильный динамический тип, а *(Foo*)nullptr определенно не имеет.


В частности, даже на платформах, которые используют описанную вами реализацию, вызов

fooObj->func();

конвертируется в

__assume(fooObj); Foo_func(fooObj);

и является нестабильным при оптимизации.

Вот пример, который будет работать вопреки вашим ожиданиям:

int main()
{
    Foo* fooObj = nullptr;
    fooObj->func();
    if (fooObj) {
        fooObj->member = 5; // This will cause a read access violation!
    }
}

В реальных системах это может привести к нарушению прав доступа к прокомментированной строке, потому что компилятор использовал тот факт, что fooObj не может быть нулевым в fooObj->func(), чтобы исключить if тест, следующий за ним.

Не делайте UB вещей, даже если вы думаете, что знаете, что делает ваша платформа. Нестабильность оптимизации реальна.


Кроме того, Стандарт еще более строг, чем вы думаете. Это также вызовет UB:

struct Foo
{
    int member;
    void func() { std::cout << "hello";}
    static void s_func() { std::cout << "greetings";}
};

int main()
{
    Foo* fooObj = nullptr;
    fooObj->s_func(); // well-formed call to static member,
         // but unlike Foo::s_func(), it requires *fooObj to be a valid object of type Foo
}

Соответствующие части Стандарта находятся в [expr.ref]:

Выражение E1->E2 преобразуется в эквивалентную форму (*(E1)).E2

и сопровождающая сноска

Если вычисляется выражение доступа к члену класса, оценка подвыражения происходит, даже если результат не нужен для определения значения всего постфиксного выражения, например, если id-expression обозначает статический член.

Это означает, что рассматриваемый код определенно оценивает (*fooObj), пытаясь создать ссылку на несуществующий объект. Было несколько предложений сделать это разрешенным и запретить только преобразование lvalue-> rvalue для такой ссылки, но до сих пор они были отклонены; даже формирование ссылки является незаконным во всех версиях Стандарта на сегодняшний день.

person Ben Voigt    schedule 10.03.2018
comment
Вы можете процитировать всемогущий Стандарт? - person Jive Dadson; 10.03.2018
comment
@JiveDadson: Готово. - person Ben Voigt; 10.03.2018
comment
В первой ссылке на вопрос, которую Lightness предоставила наиболее популярным ответом, написано TL; DR: простое разыменование нулевого указателя не вызывает UB. По этой теме ведется много споров, которые в основном сводятся к тому, является ли косвенное обращение через нулевой указатель само по себе UB. Я смущен. Может, мне не стоило задавать этот вопрос, и жизнь осталась бы проще. - person Zebrafish; 10.03.2018
comment
@Zebrafish: И в комментариях к этому ответу вы обнаружите, что Коломбо основывал свой ответ не на языковых правилах, а на предложениях (которые были отклонены) - person Ben Voigt; 10.03.2018
comment
Хммм, действительно загадочно и непонятно, но спасибо. Рассмотрю УБ. Забавно, потому что я впервые наткнулся на это из уважаемого учебника, думаю, любой может ошибаться. - person Zebrafish; 10.03.2018
comment
@Zebrafish: В самом стандарте говорится (хотя это всего лишь примечание): [Примечание: в частности, пустая ссылка не может существовать в четко определенной программе, потому что единственный способ создать такую ​​ссылку - это привязать ее к «Объект», полученный косвенным путем через нулевой указатель, что приводит к неопределенному поведению. Как описано в 12.2.4, ссылку нельзя напрямую привязать к битовому полю. - конец примечания] - person Ben Voigt; 10.03.2018
comment
да, значит, это не просто артефакт ... Я видел ошибку, которая была вызвана аналогичной ситуацией, когда if () не работал в оптимизированной версии. В основном потому, что какой-то паршивый рефакторинг изменил метод со статического на нестатический (см. Ваш комментарий ниже, контрпродуктивная ситуация) - person Swift - Friday Pie; 10.03.2018
comment
@Swift: Но как статические, так и нестатические случаи являются UB, если доступ осуществляется через ->. Вполне возможно, что компилятор использует UB по-разному. - person Ben Voigt; 10.03.2018
comment
@BenVoigt Да! Но они послушно изменились с :: на - ›после удаления статики .. Но неуместны if (), который проверяет, равен ли указатель нулю. - person Swift - Friday Pie; 10.03.2018
comment
@Swift: Ах, да, это будет проблемой. - person Ben Voigt; 10.03.2018
comment
@Zebrafish: Может, мне не стоило задавать этот вопрос, и жизнь осталась бы проще :) - person Lightness Races in Orbit; 10.03.2018
comment
@BenVoigt: К сожалению, мы не можем обязательно использовать это в качестве доказательства по двум причинам: (а) это ненормативная информация и (б) предполагаемые противоречия, как сообщается, в первую очередь связаны со стандартными дефектами. Но лично я согласен с вами в том, что намерение может быть чем-то еще, кроме этого! - person Lightness Races in Orbit; 10.03.2018
comment
@LightnessRacesinOrbit: есть четкое правило, касающееся нестатического случая, добавленное в начало ответа. - person Ben Voigt; 11.03.2018
comment
@Zebrafish: Жизнь стала простой, по крайней мере, для нестатического случая, о котором вы спрашивали. - person Ben Voigt; 11.03.2018
comment
@BenVoigt: Продолжаю играть адвоката дьявола, потому что почему бы и нет, я не верю, что формулировка актуальна. Он сообщает нам только то, что происходит, когда функция-член вызывается для объекта, принадлежащего одной из двух категорий (не к типу X или не к типу, производному от X) - он не сообщает нам, что происходит, когда функция-член вызывается для чего-либо еще (например, в этом случае ничего или, по крайней мере, не-объект). Если нигде не найдено эквивалентной формулировки для случая не-объекта, то мы, по крайней мере, можем сделать вывод о существовании UB по пропуску. - person Lightness Races in Orbit; 11.03.2018
comment
@LightnessRacesinOrbit: с -> аргумент «нет объекта» не работает. Это эквивалентно ((*E1).E2), а унарный * сообщает нам, что результатом является lvalue, относящийся к объекту или функции, на которые указывает выражение. Поскольку E1 имеет тип Foo*, это не указатель на функцию, поэтому мы можем игнорировать ветвь функции or. Выражение должно указывать на объект. (И есть запрет на применение * к нулевому указателю) - person Ben Voigt; 11.03.2018
comment
@BenVoigt: Да, это гораздо лучший аргумент для целей формирования доказательства. Опять же по пропуску поведение * здесь не определено, поскольку нет объекта или функции, на которые указывает выражение. Но эта другая формулировка (выделенная жирным шрифтом в ответе) не работает. - person Lightness Races in Orbit; 11.03.2018
comment
@LightnessRacesinOrbit: Цитата, которую я только что привел из Стандарта, также является дефектом, поскольку правила времени жизни объекта четко говорят нам, что указатели и ссылки привязаны к хранилищу, а не к объектам. Но вот оно ... - person Ben Voigt; 11.03.2018
comment
@BenVoigt: Полный круг! Если я правильно помню, это наблюдение легло в основу аргументов против здравого смысла или, по крайней мере, в основу некоторых из них. - person Lightness Races in Orbit; 11.03.2018
comment
И есть запрет на применение * к нулевому указателю Последнее, что я проверил, такой нормативной вещи не существует - для этого мы должны полагаться на более общий UB в соответствии с только что приведенной вами формулировкой - однако я Стандартные разработки устарели на четыре года, так что это могло измениться, или, возможно, я вообще неправильно запомнил. Конечно, это не имеет значения. - person Lightness Races in Orbit; 11.03.2018
comment
@LightnessRacesinOrbit: какая бы дыра могла существовать, это не относится к нулевым указателям, потому что они не идентифицируют хранилище или место, где объект может быть в другое время (но его время жизни не началось или уже закончилось). И этот объект будет здесь, в другое время правила специально запрещают доступ к нестатическим членам в то время, когда объект не существует. - person Ben Voigt; 11.03.2018
comment
В любом случае, теперь, когда мы воссоздали всю историю дебатов (несмотря на то, что на самом деле я не возражаю!), Я могу пойти приготовить ужин. :) - person Lightness Races in Orbit; 11.03.2018

На практике это обычно то, как основные компиляторы реализуют функции-члены, да. Это означает, что ваша тестовая программа, вероятно, будет работать «нормально».

Сказав это, разыменование указателя, указывающего на nullptr, является неопределенным поведением, что означает, что все ставки отключены, а вся программа и ее вывод бессмысленны, может случиться все, что угодно.

Вы никогда не можете полагаться на такое поведение, оптимизаторы, в частности, могут испортить весь этот код, потому что им разрешено предполагать, что fooObj никогда не nullptr.

person Hatted Rooster    schedule 10.03.2018

По стандарту компилятор не обязан реализовывать функцию-член, передавая ей указатель на экземпляр класса. Да, есть псевдо-указатель this, но это не связанный элемент, который гарантированно будет «понят».

nullptr указатель не указывает на какой-либо существующий объект, а -> () вызывает член этого объекта. С точки зрения стандарта, это чушь, и результат такой операции не определен (и потенциально может быть катастрофическим).

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

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

Foo::function(); 
person Swift - Friday Pie    schedule 10.03.2018
comment
Доступ к статическому члену может выглядеть так, но в C ++, в отличие от некоторых других языков, операторы . и -> также могут использоваться со статическими членами (иногда удобно в коде шаблона, когда вы либо не знать, статична ли вещь, или тип - боль в шее называть, но у вас есть экземпляр под рукой) - person Ben Voigt; 10.03.2018
comment
@BenVoigt Да, мог бы здесь лучше подойти, статический член все еще является членом - person Swift - Friday Pie; 10.03.2018