Может ли соответствующая реализация С#define NULL быть чем-то дурацким

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

Попытка провести серьезную дискуссию по-назад, используя комментарии под другими людьми, не просто и не весело. Поэтому я хотел бы услышать, что думают наши эксперты C, не ограничиваясь 500 персонажами за раз.

В стандарте C есть несколько слов, которые можно сказать о NULL и константах нулевого указателя. Там только два соответствующих раздела, которые я могу найти. Во-первых:

3.2.2.3 Указатели

Интегральное постоянное выражение с значение 0 или такое выражение cast to type void *, называется нулевым постоянная указателя. Если нулевой указатель константа присваивается или сравнивается для равенства указателю, константа преобразуется в указатель этот тип. Такой указатель, называемый null указатель, гарантируется сравнение не соответствует указателю на какой-либо объект или функция.

и второй:

4.1.5 Общие определения

Макросы

NULL

который расширяется до константы нулевого указателя, определяемой реализацией;

Вопрос в том, может ли NULL развернуть до константы указателя нулевой указателя, которая отличается от тех, которые перечислены в 3.2.2.3?

В частности, можно ли это определить как:

#define NULL __builtin_magic_null_pointer

Или даже:

#define NULL ((void*)-1)

Мое чтение 3.2.2.3 состоит в том, что он указывает, что интегральное постоянное выражение 0 и целочисленное постоянное выражение 0, передаваемого типу void *, должны быть среди форм константы нулевого указателя, которые реализует реализация, но что это не является исчерпывающим списком. Я считаю, что реализация может распознавать другие исходные конструкции как константы нулевого указателя, если никакие другие правила не нарушаются.

Так, например, доказывается, что

#define NULL (-1)

не является юридическим определением, поскольку в

if (NULL) 
   do_stuff(); 

do_stuff() не следует вызывать, тогда как

if (-1)
   do_stuff();

do_stuff() должен быть вызван; поскольку они эквивалентны, это не может быть юридическим определением NULL.

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

if ((void*)-1) 

будет оцениваться как false, и все будет хорошо.

Так что думают другие люди?

Я прошу всех особенно помнить правило "как есть", описанное в 2.1.2.3 Program execution. Это огромная и несколько круглая, поэтому я не буду вставлять ее здесь, но по сути говорит, что реализация просто должна производить те же наблюдаемые побочные эффекты, какие требуются от абстрактной машины, описанной в стандарте. В нем говорится, что любые оптимизации, преобразования или что-то еще, что компилятор хочет сделать с вашей программой, совершенно легальны, пока наблюдаемые побочные эффекты программы не будут изменены ими.

Итак, если вы хотите доказать, что конкретное определение NULL не может быть законным, вам нужно придумать программу, которая может это доказать. Либо один, как мой, который нагло нарушает другие предложения в стандарте, или тот, который может юридически обнаружить любую магию, которую должен выполнить компилятор, чтобы сделать странное определение NULL.

Стив Джессоп нашел пример того, как программа обнаруживает, что NULL не определена как одна из двух форм констант нулевого указателя в 3.2.2.3, которая заключается в том, чтобы выровнять константу:

#define stringize_helper(x) #x
#define stringize(x) stringize_helper(x) 

Используя этот макрос, можно было

puts(stringize(NULL));

и "обнаружить", что NULL не распространяется на одну из форм в 3.2.2.3. Достаточно ли этого сделать другие определения незаконными? Я просто не знаю.

Спасибо!

Ответ 1

В стандарте C99 в §7.17.3 указано, что NULL "расширяется до определенной константы нулевой указатель реализации". Между тем в §6.3.2.3.3 определяется константа нулевого указателя как "целочисленное константное выражение со значением 0 или такое выражение, которое применяется к типу void *". Поскольку нет другого определения для константы нулевого указателя, соответствующее определение NULL должно расширяться до целочисленного постоянного выражения с нулевым значением (или это отличено от void *).

Дальнейшее цитирование из C FAQ вопрос 5.5 (выделено мной):

В разделе 4.1.5 стандарта C указано, что NULL "расширяется до константы нулевого указателя, определяемой реализацией", что означает, что реализация получает выбор, какую форму 0 использовать, и использовать ли "void *"; см. вопросы 5.6 и 5.7. "Реализация, определенная здесь" здесь не означает, что NULL может быть #defined для соответствия некоторому ненулевому внутреннему значению нулевого указателя.

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

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

Ответ 2

Это немного расширяет некоторые другие ответы и делает некоторые моменты, которые другие пропустили.

Цитаты относятся к N1570 проекту стандарта ISO C 2011 года. Я не верю, что в этой области произошли значительные изменения со времени стандарта ANSI C 1989 года (что эквивалентно стандарту ISO ISO 1990). Ссылка, подобная "7.19p3", относится к подразделу 7.19, абзац 3. (Цифры в вопросе, похоже, относятся к стандарту ANSI 1989 года, в котором описывается язык в разделе 3 и в библиотеке в разделе 4, все выпуски стандарта ISO описать язык в разделе 6 и библиотеку в разделе 7.)

7.19p3 требует, чтобы макрос NULL расширялся до "константы нулевой указателя, определенной реализацией".

6.3.2.3p3 говорит:

Целочисленное постоянное выражение со значением 0 или такое выражение cast to type void *, называется константой нулевого указателя.

Так как константа нулевого указателя выделена курсивом, определение термина (3p1 указывает это соглашение) - это означает, что ничего другого, что указано, может быть константой нулевого указателя. (Стандарт не всегда строго следует этому соглашению для его определений, но нет никаких проблем, предполагая, что он делает это в этом случае.)

Итак, если мы "что-то неловкое", нам нужно посмотреть, что может быть "целочисленным постоянным выражением".

Константа нулевой указатель фразы должна приниматься как один термин, а не как фраза, смысл которой зависит от ее составных слов. В частности, целочисленная константа 0 является константой нулевого указателя, независимо от контекста, в котором она появляется; это не должно приводить к значению нулевого указателя, а к типу int, а не к типу указателя.

"Целочисленное постоянное выражение со значением 0" может быть любым из нескольких вещей (бесконечно много, если мы игнорируем пределы емкости). Литерал 0 является наиболее очевидным. Другие возможности: 0x0, 00000, 1-1, '\0' и '-'-'-'. (Из 100 слов ясно, что "значение 0" относится конкретно к этому значению типа int, но я думаю, что консенсус в том, что 0L также является допустимой константой нулевого указателя.)

Другое соответствующее предложение - 6.6p10:

Реализация может принимать другие формы постоянных выражений.

Не совсем понятно (мне), насколько широта это предназначено для разрешения. Например, компилятор может поддерживать бинарные литералы как расширение; то 0b0 будет действительной константой нулевого указателя. Это может также позволить ссылки на стиль С++ для объектов const, так что данный

const int x = 0;

ссылка на x может быть постоянным выражением (оно не находится в стандарте C).

Итак, ясно, что 0 - это константа нулевого указателя и что это правильное определение для макроса NULL.

В равной степени ясно, что (void*)0 является константой нулевого указателя, но это не допустимое определение для NULL, из-за 7.1.2p5:

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

Если NULL расширен до (void*)0, то выражение sizeof NULL будет синтаксической ошибкой.

Так что насчет ((void*)0)? Ну, я на 99.9% уверен, что он должен быть корректным определением для NULL, но 6.5.1, который описывает заключенные в скобки выражения, говорит:

Выражение в скобках является основным выражением. Его тип и стоимость идентичны выражениям несферообразного выражения. Это lvalue, обозначение функции или выражение void, если unparenthesized выражение, соответственно, lvalue, функция указатель или выражение void.

Он не говорит, что константа нулевого указателя в скобках - это константа нулевого указателя. Тем не менее, насколько я знаю, все компиляторы C разумно предполагают, что константа нулевого указателя в скобках является константой нулевого указателя, делая ((void*)0 допустимым определением для NULL.

Что делать, если нулевой указатель представлен не как все биты-ноль, а как некоторые другие битовые шаблоны, например, один эквивалент 0xFFFFFFFF. Тогда (void*)0xFFFFFFFF, даже если он вычисляет нулевой указатель, не является константой нулевого указателя, просто потому, что он не удовлетворяет определению этого термина.

Какие другие варианты допускаются стандартом?

Поскольку реализации могут принимать другие формы постоянного выражения, компилятор мог бы определить __null как константное выражение типа int со значением 0, позволяя либо __null, либо ((void*)__null) как определение NULL. Он также мог бы сделать __null самой константой типа указателя, но он не мог бы использовать __null как определение NULL, так как он не удовлетворяет определению в 6.3.2.3p3.

Компилятор может выполнить одно и то же, без магии компилятора, например:

enum { __null };
#define NULL __null

Здесь __null представляет собой целочисленное константное выражение типа int со значением 0, поэтому его можно использовать везде, где может использоваться константа 0.

Преимущество, определяющее NULL в терминах символа, такого как __null, заключается в том, что компилятор может затем выдавать (возможно, необязательное) предупреждение, если NULL используется в константе без указателя. Например, это:

char c = NULL; /* PLEASE DON'T DO THIS */

совершенно легально, если NULL определяется как 0; расширение NULL на некоторый распознаваемый токен, такой как __null, облегчит компилятору обнаружение этой сомнительной конструкции.

Ответ 3

Ну, я нашел способ доказать, что

#define NULL ((void*)-1)

не является юридическим определением NULL.

int main(void) 
{ 
   void (*fp)() = NULL;   
}

Инициализация указателя функции с помощью NULL является законной и правильной, тогда как...

int main(void) 
{ 
   void (*fp)() = (void*)-1;   
}

... является нарушением ограничения, которое требует диагностики. Так что.

Но определение __builtin_magic_null_pointer NULL не пострадает от этой проблемы. Я все еще хотел бы знать, может ли кто-нибудь придумать причину, почему этого не может быть.

Ответ 4

Позднее, но никто не поднял этот вопрос: предположим, что реализация фактически использует

#define NULL __builtin_null

Мое чтение C99 заключается в том, что это прекрасно, пока специальное ключевое слово __builtin_null ведет себя как - если бы оно было либо "интегральным постоянным выражением со значением 0", либо "интегральным постоянным выражением со значением 0, отличным от void *". В частности, если реализация выбирает первый из этих параметров, то

int x = __builtin_null;
int y = __builtin_null + 1;

является допустимой единицей перевода, устанавливая x и y на целые значения 0 и 1 соответственно. Если он выбирает последний, то, конечно, оба являются нарушениями ограничений (6.5.16.1, 6.5.6 соответственно; void * не является "указателем на тип объекта" в 6.2.5p19; 6.7.8p11 применяет ограничения для присвоения инициализация). И я не понимаю, почему реализация будет делать это, если бы не обеспечить лучшую диагностику "неправильного использования" NULL, поэтому представляется вероятным, что для этого потребуется вариант, который сделал бы недействительным больше кода.

Ответ 5

Интегральное постоянное выражение с значение 0 или такое выражение cast to type void *, называется константа нулевой указатель.

NULL, который расширяется до определяемый реализацией нулевой указатель константа;

поэтому либо

NULL == 0

или

NULL == (void *) 0

Ответ 6

Константа нулевого указателя должна оценивать 0, иначе выражения типа !ptr не будут работать должным образом.

Макрос NULL расширяется до 0-значного выражения; AFAIK, это всегда было.