С++ доступ к статическим членам с использованием нулевого указателя

Недавно попробовал следующую программу, и она компилируется, работает нормально и производит ожидаемый вывод вместо любой ошибки времени выполнения.

#include <iostream>
class demo
{
    public:
        static void fun()
        {
            std::cout<<"fun() is called\n";
        }
        static int a;
};
int demo::a=9;
int main()
{
    demo* d=nullptr;
    d->fun();
    std::cout<<d->a;
    return 0;
}

Если для доступа к классам и/или структурам используется неинициализированный указатель, это undefined, но почему ему также разрешен доступ к статическим членам с использованием нулевых указателей. Есть ли вред в моей программе?

Ответ 1

TL; DR. Ваш пример четко определен. Простое разыменование нулевого указателя не вызывает UB.

Существует много споров по этой теме, которая в основном сводится к тому, является ли косвенным путем нулевой указатель сам UB.
Единственное сомнительное, что происходит в вашем примере, - это оценка выражения объекта. В частности, d->a эквивалентно (*d).a в соответствии с [expr.ref]/2:

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

*d оценивается только:

Постфиксное выражение перед точкой или стрелкой оценивается: 65результат этой оценки вместе с id-выражением определяет результат всего постфиксного выражения.

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

Позвольте извлечь критическую часть кода. Рассмотрим оператор выражения

*d;

В этом выражении *d - это отброшенное выражение в соответствии с [stmt.expr]. Итак, *d оценивается только 1 как и в d->a.
Следовательно, если *d; действительно, или, другими словами, оценка выражения *d, так и ваш пример.

Оказывает ли косвенное обращение с помощью нулевых указателей поведение undefined?

Существует открытая проблема CWG # 232, созданная более пятнадцати лет назад, что касается этого точного вопроса. Возникает очень важный аргумент. Отчет начинается с

По крайней мере, несколько мест в состоянии IS, которые направляют через null pointer производит поведение undefined: 1.9 [intro.execution] в пункте 4 дается "разыменование нулевого указателя" в качестве примера undefined и 8.3.2 [dcl.ref], пункт 4 (в примечании) использует это предположительно undefined поведение как оправдание для несуществование "нулевых ссылок".

Обратите внимание, что упомянутый пример был изменен для изменения модификаций объектов const, а примечание в [dcl.ref] - пока все еще существует - не является нормативным. Нормативный проход был удален, чтобы избежать обязательств.

Однако, пункт 5.3.1 [expr.unary.op], который описывает унарный "*", не говорит, что поведение undefined, если операнд - нулевой указатель, как и следовало ожидать. Кроме того, по крайней мере один проход дает разыменование корректного поведения нулевого указателя: 5.2.8 [expr.typeid], пункт 2, говорит

Если выражение lvalue получается путем применения унарного * оператора   указателю, а указатель - нулевым значением указателя (4.10   [conv.ptr]), выражение typeid вызывает исключение bad_typeid   (18.7.3 [bad.typeid]).

Это несовместимо и должно быть очищено.

Последний пункт особенно важен. Цитата в [expr.typeid] все еще существует и содержит glvalues ​​типа полиморфного класса, что имеет место в следующем примере:

int main() try {

    // Polymorphic type
    class A
    {
        virtual ~A(){}
    };

    typeid( *((A*)0) );

}
catch (std::bad_typeid)
{
    std::cerr << "bad_exception\n";
}

Поведение этой программы хорошо определено (исключение будет выбрано и выхвачено), а выражение *((A*)0) оценивается, поскольку оно не является частью неоцененного операнда. Теперь, если косвенность через нулевые указатели индуцировала UB, тогда выражение, записанное как

*((A*)0);

будет делать именно это, вызывая UB, что кажется бессмысленным по сравнению с сценарием typeid. Если вышеприведенное выражение просто оценивается, поскольку каждое выражение с отброшенным значением является 1 где критическое различие, которое делает оценку во втором фрагменте UB? Нет существующей реализации который анализирует typeid -operand, находит самую внутреннюю, соответствующую разыменованность и окружает ее операнд с проверкой - также будет потеря производительности.

Затем в этом выпуске заканчивается короткое обсуждение:

Мы согласились, что подход в стандарте выглядит нормально: p = 0; *p;не является по своей сути ошибкой. Преобразование lvalue-to-rval даст это поведение undefined.

т.е. комитет согласился с этим. Хотя предлагаемая резолюция этого доклада, в которой вводились так называемые "пустые слова", никогда не принималась...

Однако "не модифицируемый" - это концепция времени компиляции, а на самом деле это касается значений времени выполнения и, следовательно, должно производить undefinedповедение. Кроме того, существуют и другие контексты, в которых lvalues ​​может происходят, например, левый операнд. или. *, которые также должны быть ограничено. Требуется дополнительная подготовка.

... , что не влияет на логику. Опять же, следует отметить, что эта проблема даже предшествует С++ 03, что делает ее менее убедительной, когда мы приближаемся к С++ 17.


CWG-issue # 315, похоже, также охватывает ваш случай:

Еще один пример для рассмотрения - вызов функции-члена от нулевого указателя:

  struct A { void f () { } };
  int main ()
  {
    A* ap = 0;
    ap->f ();
  }

[...]

Обоснование (октябрь 2003 г.):

Мы согласились, что пример должен быть разрешен. p->f() переписывается как (*p).f() в соответствии с 5.2.5 [expr.ref]. *p не является ошибкой, когда p имеет значение null, если значение lvalue не преобразуется в значение r (4.1 [conv.lval]), которого нет здесь.

В соответствии с этим обоснованием косвенность через нулевой указатель сама по себе не вызывает UB без дальнейших преобразований lvalue-to-rvalue (= обращается к сохраненному значению), привязки ссылок, вычисления значений или тому подобное. (Nota bene: вызов нестатической функции-члена с нулевым указателем должен вызывать UB, хотя и просто туманно запрещен [class.mfct.non-static]/2. Обоснование устарело в этом отношении.)

т.е. простой оценки *d недостаточно для вызова UB. Идентификация объекта не требуется, и ни одно из них не является ранее сохраненным значением. С другой стороны, например,

*p = 123;

есть undefined, так как вычисляется значение левого операнда, [expr.ass]/1:

Во всех случаях назначение упорядочивается после вычисления значения правого и левого операндов

Поскольку левый операнд, как ожидается, будет значением glvalue, идентификация объекта, на который ссылается этот glvalue, должна определяться, как указано в определении оценки выражения в [intro.execution]/12, что невозможно ( и, таким образом, приводит к UB).


1 [expr]/11:

В некоторых контекстах выражение появляется только для его побочных эффектов. Такое выражение называется выражением отбрасываемого значения. The выражение оценивается и его значение отбрасывается. [...]. Преобразование lvalue-to-rvalue (4.1) является применяется тогда и только тогда, когда выражение является значением gl нестабильный тип и [...]

Ответ 2

Из проекта С++ Draft N3337:

9.4 Статические элементы

2 A static член s класса X может ссылаться на выражение с выражением-id X::s; нет необходимости использовать синтаксис доступа членов класса (5.2.5) для ссылки на член static. Член A static может быть передан к использованию синтаксиса доступа к члену класса, и в этом случае выражение объекта оценивается.

А в разделе об объектном выражении...

5.2.5 Доступ к члену класса

4 Если объявлено, что E2 имеет тип "ссылка на T", тогда E1.E2 является lvalue; тип E1.E2 равен T. В противном случае, применяется одно из следующих правил.

- Если E2 является членом данных static, а тип E2 равен T, то E1.E2 является значением l; выражение обозначает именованный член класса. Тип E1.E2 - T.

В соответствии с последним абзацем стандарта выражения:

  d->fun();
  std::cout << d->a;

работают, потому что оба они обозначают именованный элемент класса независимо от значения d.

Ответ 3

работает нормально и производит ожидаемый результат вместо любой ошибки времени выполнения.

Это базовая ошибка допущения. То, что вы делаете, это undefined поведение, что означает, что ваша претензия в отношении любого вида "ожидаемого вывода" неисправна.

Добавление: Обратите внимание, что при наличии дефекта CWG (#315), который закрыт как "в согласии" с несоблюдением вышеуказанного UB, он полагается на положительное закрытие другого дефекта CWG (#232), который по-прежнему активен, и, следовательно, ни один из них не добавляется к стандарту.

Позвольте мне привести часть комментария Джеймс Макнеллис к ответ к аналогичному вопросу:

Я не думаю, что дефект CWG 315 "закрыт", поскольку его присутствие на странице "закрытые проблемы" подразумевает. В обосновании говорится, что это должно быть разрешено, потому что "* p не является ошибкой, когда p равно null, если значение lvalue не преобразуется в значение r". Однако это опирается на концепцию "пустой lvalue", которая является частью предлагаемой резолюции для дефекта CWG 232, но которая не была принята.

Ответ 4

То, что вы видите здесь, - это то, что я считаю непродуманным и неудачным выбором дизайна в спецификации языка С++ и многих других языков, принадлежащих к одному и тому же общему семейству языков программирования.

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

Итак, в d->fun(); компилятор использует указатель d только во время компиляции, чтобы выяснить, что вы имеете в виду член класса demo, а затем он игнорирует его. Компилятор не генерирует код для разыменования указателя, поэтому факт, что он будет NULL во время выполнения, не имеет значения.

Итак, то, что вы видите, находится в полном соответствии с спецификацией языка, и, на мой взгляд, спецификация страдает в этом отношении, потому что это позволяет сделать нелогичную вещь: использовать ссылку на экземпляр для ссылки на статическую член.

P.S. Большинство компиляторов на большинстве языков на самом деле способны выдавать предупреждения для такого рода материалов. Я не знаю о вашем компиляторе, но вы можете проверить, потому что тот факт, что вы не получили предупреждения о том, что вы делали, может означать, что у вас недостаточно предупреждений.