¿Puede una implementación C conforme #define NULL ser algo loco?

Pregunto por la discusión que se ha provocado en este hilo .

Tratar de tener una conversación seria con los comentarios bajo las respuestas de otras personas no es fácil ni divertido. Así que me gustaría escuchar lo que piensan nuestros expertos en C sin estar restringido a 500 caracteres a la vez.

El estándar C tiene pocas palabras valiosas para decir sobre NULL y constantes de puntero nulo. Solo hay dos secciones relevantes que puedo encontrar. Primero:

3.2.2.3 Punteros

Una expresión de constante integral con el valor 0, o una expresión de este tipo para escribir void *, se llama constante de puntero nulo. Si se asigna una constante de puntero nulo o se compara la igualdad con un puntero, la constante se convierte en un puntero de ese tipo. Se garantiza que este puntero, llamado puntero nulo, se compare desigual con un puntero a cualquier objeto o función.

y segundo:

4.1.5 Definiciones comunes

Las macros son

 NULL 

que se expande a una constante de puntero nulo definido por la implementación;

La pregunta es, ¿puede NULL expandirse a una constante de puntero nulo definida por la implementación que es diferente de las enumeradas en 3.2.2.3?

En particular, ¿podría definirse como:

 #define NULL __builtin_magic_null_pointer 

O incluso:

 #define NULL ((void*)-1) 

Mi lectura de 3.2.2.3 es que especifica que una expresión constante integral de 0 y una expresión de constante integral de 0 para escribir void * debe estar entre las formas de constante de puntero nulo que la implementación reconoce, pero que no es pretende ser una lista exhaustiva. Creo que la implementación es libre de reconocer otras construcciones fuente como constantes de puntero nulo, siempre que no se rompan otras reglas.

Entonces, por ejemplo, es demostrable que

 #define NULL (-1) 

no es una definición legal, porque en

 if (NULL) do_stuff(); 

do_stuff() no debe llamarse, mientras que con

 if (-1) do_stuff(); 

do_stuff() debe ser llamado; ya que son equivalentes, esta no puede ser una definición legal de NULL .

Pero el estándar dice que las conversiones de entero a puntero (y viceversa) están definidas por la implementación, por lo tanto, podría definir la conversión de -1 a un puntero como una conversión que produce un puntero nulo. En ese caso

 if ((void*)-1) 

evaluaría a falso, y todo estaría bien.

Entonces, ¿qué piensan los demás?

Le pediría a todo el mundo que tenga especialmente en cuenta la regla “como si” descrita en 2.1.2.3 Program execution . Es enorme y un poco indirecto, así que no lo pegaré aquí, pero básicamente dice que una implementación simplemente tiene que producir los mismos efectos secundarios observables que se requieren de la máquina abstracta descrita por el estándar. Dice que cualquier optimización, transformación o cualquier otra cosa que el comstackdor quiera hacer a su progtwig es perfectamente legal, siempre y cuando los efectos colaterales observables del progtwig no sean modificados por ellos.

Entonces, si estás buscando demostrar que una definición particular de NULL no puede ser legal, necesitarás encontrar un progtwig que pueda probarlo. O bien uno como el mío rompe flagrantemente otras cláusulas del estándar, o uno que puede detectar legalmente cualquier magia que el comstackdor tenga que hacer para hacer funcionar la extraña definición NULL.

Steve Jessop encontró un ejemplo de cómo un progtwig puede detectar que NULL no está definido como una de las dos formas de constantes de puntero nulo en 3.2.2.3, que consiste en curvar la constante:

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

Usando esta macro, uno podría

 puts(stringize(NULL)); 

y “detectar” que NULL no se expande a una de las formas en 3.2.2.3. ¿Es eso suficiente para que otras definiciones sean ilegales? Simplemente no sé.

¡Gracias!

    En el estándar C99, §7.17.3 establece que NULL “se expande a una constante de puntero nulo definida por la implementación”. Mientras tanto, §6.3.2.3.3 define la constante de puntero nulo como “una expresión constante entera con el valor 0, o una expresión de este tipo para escribir void * “. Como no hay otra definición para una constante de puntero nulo, una definición conforme de NULL debe expandirse a una expresión constante entera con el valor cero (o esta conversión a void * ).

    Cita adicional de la pregunta 5.5 de C FAQ (énfasis agregado):

    La Sección 4.1.5 del Estándar C establece que NULL “se expande a una constante de puntero nulo definida por la implementación”, lo que significa que la implementación puede elegir qué forma de 0 usar y si usar un molde `void *`; ver preguntas 5.6 y 5.7. “Definido por la implementación” aquí no significa que NULL pueda #definirse para que coincida con algún valor de puntero nulo interno no nulo específico de la implementación .

    Tiene perfecto sentido; dado que el estándar requiere una constante entera cero en contextos de apuntador para comstackr en un puntero nulo (independientemente de si la representación interna de la máquina tiene un valor de cero), el caso donde NULL se define como cero debe manejarse de cualquier manera. El progtwigdor no está obligado a escribir NULL para obtener punteros nulos; es solo una convención estilística (y puede ayudar a detectar errores, por ejemplo, cuando se usa un NULL definido como (void *)0 en un contexto sin puntero).

    Editar: Una fuente de confusión aquí parece ser el lenguaje conciso utilizado por el estándar, es decir, no dice explícitamente que no hay ningún otro valor que pueda considerarse una constante de puntero nulo. Sin embargo, cuando el estándar dice “… se llama una constante de puntero nulo”, significa que exactamente las definiciones dadas se llaman constantes de puntero nulo. No es necesario que se siga explícitamente cada definición al establecer lo que no es conforme cuando (por definición) el estándar define lo que se está conformando.

    Esto se expande un poco en algunas de las otras respuestas y hace algunos puntos que los otros se perdieron.

    Las citas son para N1570 un borrador del estándar ISO C 2011. No creo que haya habido cambios significativos en esta área desde el estándar ANSI C de 1989 (que es equivalente al estándar ISO C de 1990). Una referencia como “7.19p3” se refiere a la subsección 7.19, párrafo 3. (Las citas en la pregunta parecen estar en la norma ANSI de 1989, que describía el lenguaje en la sección 3 y la biblioteca en la sección 4; todas las ediciones de la norma ISO describa el lenguaje en la sección 6 y la biblioteca en la sección 7.)

    7.19p3 requiere que la macro NULL expanda a “una constante de puntero nulo definido por la implementación”.

    6.3.2.3p3 dice:

    Una expresión constante entera con el valor 0, o una expresión de este tipo para escribir void * , se llama constante de puntero nulo .

    Como la constante del puntero nulo está en cursiva, esa es la definición del término (3p1 especifica esa convención), lo que implica que nada más que lo que allí se especifica puede ser una constante del puntero nulo. (El estándar no siempre sigue estrictamente esa convención para sus definiciones, pero no hay ningún problema si se supone que lo hace en este caso).

    Entonces, si somos “algo loco”, tenemos que ver qué puede ser una “expresión constante entera”.

    La frase constante de puntero nulo debe tomarse como un único término, no como una frase cuyo significado depende de sus palabras constituyentes. En particular, la constante 0 entera es una constante de puntero nulo, independientemente del contexto en el que aparece; no necesita dar como resultado un valor de puntero nulo, y es de tipo int , no de cualquier tipo de puntero.

    Una “expresión constante entera con el valor 0” puede ser cualquiera de varias cosas (infinitamente muchas si ignoramos los límites de capacidad). Un literal 0 es el más obvio. Otras posibilidades son 0x0 , 00000 , 1-1 , '\0' y '-'-'-' . (No está 100% claro en la redacción si “el valor 0” se refiere específicamente a ese valor de tipo int , pero creo que el consenso es que 0L también es una constante de puntero nulo válida.)

    Otra cláusula relevante es 6.6p10:

    Una implementación puede aceptar otras formas de expresiones constantes.

    No está del todo claro (para mí) la cantidad de latitud que se pretende permitir. Por ejemplo, un comstackdor puede admitir literales binarios como una extensión; entonces 0b0 sería una constante de puntero nulo válida. También podría permitir referencias de estilo C ++ a objetos const , por lo que

     const int x = 0; 

    una referencia a x podría ser una expresión constante (no está en el estándar C).

    Por lo tanto, está claro que 0 es una constante de puntero nulo y que es una definición válida para la macro NULL .

    Está igualmente claro que (void*)0 es una constante de puntero nulo, pero no es una definición válida para NULL , debido a 7.1.2p5:

    Cualquier definición de una macro similar a un objeto descrita en esta cláusula se expandirá a un código que esté totalmente protegido por paréntesis cuando sea necesario, de modo que agrupe en una expresión arbitraria como si fuera un único identificador.

    Si NULL expandió a (void*)0 , entonces el sizeof NULL expresión de sizeof NULL sería un error de syntax.

    ¿Y qué hay de ((void*)0) ? Bueno, estoy 99.9% seguro de que se pretende que sea una definición válida para NULL , pero 6.5.1, que describe expresiones entre paréntesis, dice:

    Una expresión entre paréntesis es una expresión primaria. Su tipo y valor son idénticos a los de la expresión sin parentesis. Es un lvalue, un designador de función o una expresión vacía si la expresión sin parche es, respectivamente, un lvalue, un designador de función o una expresión vacía.

    No dice que una constante de puntero nulo entre paréntesis sea una constante de puntero nulo. Aún así, hasta donde sé, todos los comstackdores de C suponen razonablemente que una constante de puntero nulo entre paréntesis es una constante de puntero nulo, lo que hace que ((void*)0 una definición válida para NULL .

    ¿Qué 0xFFFFFFFF si un puntero nulo se representa no como todos los bits cero, sino como algún otro patrón de bits, por ejemplo, uno equivalente a 0xFFFFFFFF ? Entonces (void*)0xFFFFFFFF , incluso si resulta que evaluar a un puntero nulo no es una constante de puntero nulo, simplemente porque no satisface la definición de ese término.

    Entonces, ¿qué otras variaciones están permitidas por el estándar?

    Como las implementaciones pueden aceptar otras formas de expresión constante, un comstackdor podría definir __null como una expresión constante de tipo int con el valor 0 , lo que permite __null o ((void*)__null) como la definición de NULL . También podría hacer que __null sea ​​una constante de tipo puntero , pero no podría usar __null como la definición de NULL , ya que no cumple con la definición en 6.3.2.3p3.

    Un comstackdor podría lograr lo mismo, sin magia de comstackción, así:

     enum { __null }; #define NULL __null 

    Aquí __null es una expresión constante entera de tipo int con el valor 0 , por lo que puede usarse en cualquier lugar donde se pueda usar una constante 0 .

    La ventaja que define a NULL en términos de un símbolo como __null es que el comstackdor podría emitir una advertencia (quizás opcional) si se utiliza NULL en una constante no puntero. Por ejemplo, esto:

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

    es perfectamente legal si NULL pasa a definirse como 0 ; expandir NULL a algún token reconocible como __null facilitaría al comstackdor detectar este constructo cuestionable.

    Bueno, he encontrado una manera de probar eso

     #define NULL ((void*)-1) 

    no es una definición legal de NULL.

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

    Inicializar un puntero de función con NULL es legal y correcto, mientras que …

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

    … es una violación de restricción que requiere un diagnóstico. Así que eso está fuera.

    Pero la definición __builtin_magic_null_pointer de NULL no sufriría ese problema. Todavía me gustaría saber si alguien puede encontrar una razón por la que no puede ser.

    Algunas edades más tarde, pero nadie mencionó este punto: supongamos que la implementación de hecho elige usar

     #define NULL __builtin_null 

    Mi lectura de C99 es que eso está bien siempre y cuando la palabra clave especial __builtin_null comporte como si “fuera una expresión constante integral con valor 0” o “una expresión constante integral con valor 0, emitida como void * “. En particular, si la implementación elige la primera de esas opciones, entonces

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

    es una unidad de traducción válida, estableciendo y en los valores enteros 0 y 1 respectivamente. Si elige este último, por supuesto, ambos son violaciones de restricción (6.5.16.1, 6.5.6 respectivamente; void * no es un “puntero a un tipo de objeto” por 6.2.5p19; 6.7.8p11 aplica las restricciones para la asignación a la inicialización ) Y no veo de manera informal por qué una implementación podría hacer esto si no se proporcionan mejores diagnósticos para el “mal uso” de NULL, por lo que parece probable que tome la opción que invalida más código.

    Una expresión de constante integral con el valor 0, o una expresión de este tipo para escribir void *, se llama constante de puntero nulo .

    NULL que se expande a una constante de puntero nulo definido por la implementación;

    por lo tanto,

    NULL == 0

    o

    NULL == (void *) 0

    La constante del puntero nulo debe evaluar 0; de lo contrario, expresiones como !ptr no funcionarían como se esperaba.

    La macro NULL se expande a una expresión de 0 valores; AFAIK, siempre lo ha hecho.