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

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

#include  class demo { public: static void fun() { std::cout<fun(); std::cout<a; return 0; } 

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

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

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

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

*d оценивается просто:

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

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

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

 *d; 

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

Оказывает ли косвенное обращение через нулевые указатели неопределенное поведение?

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

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

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

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

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

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

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

 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 , где критическое различие, которое делает оценку во втором fragmentе UB? Существует не существующая реализация, которая анализирует typeid -operand, находит самую внутреннюю, соответствующую разыменованность и окружает ее операнд с проверкой - также будет потеря производительности.

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

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

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

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

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


CWG-выпуск № 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 не преобразуется в rvalue (4.1 [conv.lval]), которого нет здесь.

В соответствии с этим обоснованием косвенность посредством нулевого указателя сама по себе не вызывает UB без дополнительных преобразований lvalue-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:

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

Из проекта стандарта C ++ N3337:

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

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

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

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

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

– Если E2 является static элементом данных, а тип E2T , то E1.E2 является lvalue; выражение обозначает именованный член classа. Тип E1.E2 – T.

Исходя из последнего параграфа стандарта, выражения:

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

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

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

Это основная ошибка допущения. То, что вы делаете, – это неопределенное поведение , а это означает, что ваша претензия на любой «ожидаемый результат» является неисправной.

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

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

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

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

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

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

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

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

Давайте будем гением компьютера.