¿Cómo maneja el preprocesador C las dependencias circulares?

8 minutos de lectura

avatar de usuario
profundo

quiero saber como C el preprocesador maneja las dependencias circulares (de #defines). Este es mi programa:

#define ONE TWO 
#define TWO THREE
#define THREE ONE

int main()
{
    int ONE, TWO, THREE;
    ONE = 1;
    TWO = 2;
    THREE = 3;
    printf ("ONE, TWO, THREE = %d,  %d, %d \n",ONE,  TWO, THREE);
}

Aquí está la salida del preprocesador. No puedo entender por qué la salida es como tal. Me gustaría saber los diversos pasos que toma un preprocesador en este caso para dar el siguiente resultado.

# 1 "check_macro.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "check_macro.c"

int main()
{
 int ONE, TWO, THREE;
 ONE = 1;
 TWO = 2;
 THREE = 3;
 printf ("ONE, TWO, THREE = %d,  %d, %d \n",ONE, TWO, THREE);
}

Estoy ejecutando este programa en Linux 3.2.0-49-generic-pae y compilando en gcc versión 4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5).

avatar de usuario
rico

Mientras se expande una macro de preprocesador, el nombre de esa macro no se expande. Entonces, los tres símbolos se definen como ellos mismos:

ONE -> TWO -> THREE -> ONE (not expanded because expansion of ONE is in progress)
TWO -> THREE -> ONE -> TWO (        "                         TWO      "        )
THREE -> ONE -> TWO -> THREE (      "                         THREE    "        )

Este comportamiento está establecido por §6.10.3.4 del estándar C (número de sección del borrador C11, aunque hasta donde yo sé, la redacción y numeración de la sección no ha cambiado desde C89). Cuando se encuentra un nombre de macro, se reemplaza con su definición (y # y ## se tratan los operadores del preprocesador, así como los parámetros de las macros de función). Luego, el resultado se vuelve a escanear en busca de más macros (en el contexto del resto del archivo):

2/ Si el nombre de la macro que se reemplaza se encuentra durante este escaneo de la lista de reemplazo (sin incluir el resto de los tokens de preprocesamiento del archivo de origen), no se reemplaza. Además, si algún reemplazo anidado encuentra el nombre de la macro que se reemplaza, no se reemplaza…

La cláusula continúa diciendo que cualquier token que no se reemplace debido a una llamada recursiva está efectivamente “congelado”: nunca se reemplazará:

… Estos tokens de preprocesamiento de nombre de macro no reemplazados ya no están disponibles para un reemplazo posterior, incluso si se (re)examinan más tarde en contextos en los que ese token de preprocesamiento de nombre de macro habría sido reemplazado de otro modo.

La situación a la que se refiere la última oración rara vez surge en la práctica, pero este es el caso más simple que se me ocurrió:

#define two one,two
#define a(x) b(x)
#define b(x,y) x,y
a(two)

El resultado es one, two. two se expande a one,two durante el reemplazo de ay el ampliado two está marcado como completamente expandido. Después, b(one,two) se expande. Esto ya no está en el contexto de la sustitución de twopero el two cual es el segundo argumento de b se ha congelado, por lo que no se vuelve a expandir.

  • +1, excelente respuesta. Aquí hay un ejemplo que creo que demuestra muy bien este comportamiento (pero, por desgracia, es demasiado largo para un comentario).

    – Ilmari Karonen

    13 de junio de 2014 a las 11:46

  • @IlmariKaronen: agregué un ejemplo para la última oración del párrafo 2, que por lo demás es un poco difícil de entender. Pero al volver a leer su comentario/respuesta, no creo que eso fuera lo que pretendía, por lo que no es necesario decir que su ejemplo es aproximadamente el mismo que el OP, aunque el resultado es posiblemente un poco más visual.

    – rico

    13 de junio de 2014 a las 21:36

Su pregunta es respondida por publicación. ISO/CEI 9899:TC2 sección 6.10.3.4 “Reescaneo y reemplazo posterior”, párrafo 2, que cito aquí para su conveniencia; en el futuro, considere leer la especificación cuando tenga una pregunta sobre la especificación.

Si el nombre de la macro que se reemplaza se encuentra durante este escaneo de la lista de reemplazo (sin incluir el resto de los tokens de preprocesamiento del archivo de origen), no se reemplaza. Además, si algún reemplazo anidado encuentra el nombre de la macro que se reemplaza, no se reemplaza. Estos tokens de preprocesamiento de nombre de macro no reemplazados ya no están disponibles para un reemplazo posterior, incluso si se (re)examinan más tarde en contextos en los que ese token de preprocesamiento de nombre de macro se habría reemplazado de otro modo.

  • Para ser justos, encontrar y comprender la respuesta en el estándar C no es una tarea trivial. Con su lógica de “ir a leer el estándar”, podríamos responder todas las preguntas relacionadas con C con RTFM.

    – Lundin

    12 de junio de 2014 a las 6:54

  • @Lundin: la especificación comienza con una tabla de contenido que identifica claramente qué sección de la especificación trata sobre la expansión macro; Me tomó 30 segundos encontrar el párrafo correcto y no soy un experto en la especificación C. Y sí, con mi excelente sugerencia de que la gente realmente leen el estándar cuando tienen una pregunta sobre un lenguaje estandarizado, la mayoría de las malas preguntas de esta etiqueta desaparecerían. Eso es bueno.

    –Eric Lippert

    12 de junio de 2014 a las 7:02


  • Excepto que esta no es una mala pregunta. El OP investigó un poco, incluyó un ejemplo que compila y la salida del preprocesador, especificó el compilador y el sistema, etc. Y parece que no hay duplicados obvios de la pregunta. Nuevamente, leer el estándar C no es una tarea trivial. Por ejemplo, no lograste hacerlo. Por alguna razón, está citando un borrador N1124 a ISO 9899: 1999 TC2, que desde entonces ha sido reemplazado por C99 + TC2, C99 + TC3 borrador N1256, C99 + TC3, C11, C11 + TC1. Aunque estoy seguro de que conoce todos los cambios de la exploración de macros a través de estas revisiones…

    – Lundin

    12 de junio de 2014 a las 11:46


  • @Lundin: En cuanto a las malas preguntas, veo preguntas 100 veces peores aquí todos los días, así que sí, es bastante bueno. Elegí esa versión del estándar porque es fácil de encontrar (tiene un enlace desde Wikipedia) y es gratis, y la mayoría de los compiladores lo cumplen. Como dije, no soy un experto en absoluto en la historia o el contenido de la especificación C; mi punto es que logré encontrar una respuesta a la pregunta con una búsqueda en la web y un vistazo a la tabla de contenido; esto no está fuera del alcance de la posibilidad para el programador promedio. Animo a que la lectura de especificaciones esté en la caja de herramientas de todos los programadores.

    –Eric Lippert

    12 de junio de 2014 a las 13:47

  • @Alice: Aunque cHao tal vez podría haberse expresado de manera más elegante, el punto está bien entendido. Básicamente, no me preocupan las pruebas de corrección de los métodos de cien líneas en el código de la biblioteca que tiene una especificación clara; Me preocupa la corrección de sistemas operativos completos, bases de datos completas, etc., que operan en un mundo con modelos de memoria débiles, lenguajes inseguros para la memoria, etc. Las técnicas que utiliza para probar la corrección de las colecciones de STL no se adaptan a todo el sistema operativo Windows.

    –Eric Lippert

    25/06/2014 a las 22:20


https://gcc.gnu.org/onlinedocs/cpp/Self-Referential-Macros.html#Self-Referential-Macros responde a la pregunta sobre las macros autorreferenciales.

El quid de la respuesta es que cuando el preprocesador encuentra macros autorreferenciales, no las expande en absoluto.

Sospecho que se usa la misma lógica para evitar la expansión de macros definidas circularmente. De lo contrario, el preprocesador estará en una expansión infinita.

En su ejemplo, realiza el procesamiento de macros antes de definir las variables del mismo nombre, por lo que, independientemente del resultado del procesamiento de macros, siempre imprime 1, 2, 3!

Aquí hay un ejemplo donde las variables se definen primero:

#include <stdio.h>
int main()
{
    int A = 1, B = 2, C = 3;
#define A B
#define B C
//#define C A
    printf("%d\n", A);
    printf("%d\n", B);
    printf("%d\n", C);
}

esto imprime 3 3 3. Algo insidiosamente, sin comentar #define C A cambia el comportamiento de la línea printf("%d\n", B);

Aquí hay una buena demostración del comportamiento descrito en las respuestas de rici y Eric Lippert, es decir, que el nombre de una macro no se vuelve a expandir si se encuentra nuevamente mientras se expande la misma macro.

Contenido de test.c:

#define ONE 1, TWO
#define TWO 2, THREE
#define THREE 3, ONE

int foo[] = {
  ONE,
  TWO,
  THREE
};

salida de gcc -E test.c (excluyendo inicial # 1 ... líneas):

int foo[] = {
  1, 2, 3, ONE,
  2, 3, 1, TWO,
  3, 1, 2, THREE
};

(Publicaría esto como un comentario, pero incluir bloques de código sustanciales en los comentarios es un poco incómodo, por lo que estoy haciendo de esto una respuesta Wiki de la comunidad. Si cree que sería mejor incluirlo como parte de una respuesta existente, siéntase libre para copiarlo y pedirme que elimine esta versión de CW).

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad