Mientras trabajaba en un gran proyecto lleno de trucos y hechicería con macros, me topé con un error en el que una macro no se expandía correctamente. La salida resultante fue " EXPAND(0) ", pero EXPAND se definió como " #define EXPAND(X) X ", por lo que claramente la salida debería haber sido " 0 ".
"No hay problema", pensé para mis adentros. "Probablemente sea un error tonto, hay algunas macros desagradables aquí, después de todo, muchos lugares pueden salir mal". Mientras pensaba eso, aislé las macros que se comportaban mal en su propio proyecto, unas 200 líneas, y comencé a trabajar en un MWE para identificar el problema. 200 líneas se convirtieron en 150, que a su vez se convirtieron en 100, luego en 20, 10... Para mi absoluta sorpresa, este fue mi MWE final:
#define EXPAND(X) X #define PARENTHESIS() () #define TEST() EXPAND(0) EXPAND(TEST PARENTHESIS()) // EXPAND(0)4 lineas
Para colmo de males, casi cualquier modificación a las macros hará que funcionen correctamente:
#define EXPAND(X) X #define PARENTHESIS() () #define TEST() EXPAND(0) // Manually replaced PARENTHESIS() EXPAND(TEST ()) // 0 #define EXPAND(X) X #define PARENTHESIS() () #define TEST() EXPAND(0) // Manually replaced TEST() EXPAND(EXPAND(0)) // 0 // Set EXPAND to 0 instead of X #define EXPAND(X) 0 #define PARENTHESIS() () #define TEST() EXPAND(0) EXPAND(TEST PARENTHESIS()) // 0Pero lo más importante, y lo más extraño, el siguiente código falla exactamente de la misma manera:
#define EXPAND(X) X #define PARENTHESIS() () #define TEST() EXPAND(0) EXPAND(EXPAND(EXPAND(EXPAND(TEST PARENTHESIS())))) // EXPAND(0) Esto significa que el preprocesador es perfectamente capaz de expandir EXPAND , pero por alguna razón, se niega rotundamente a expandirlo nuevamente en el último paso.
Ahora, cómo voy a resolver este problema en mi programa real no está ni aquí ni allá. Aunque una solución sería buena (es decir, una forma de expandir el token EXPAND(TEST PARENTHESIS()) a 0 ), lo que más me interesa es: ¿por qué? ¿Por qué el preprocesador de C llegó a la conclusión de que " EXPAND(0) " era la expansión correcta en el primer caso, pero no en los otros?
Aunque es fácil encontrar recursos sobre lo que hace el preprocesador C (y algo de magia que puedes hacer con él), todavía tengo que encontrar uno que explique cómo lo hace, y quiero aprovechar esta oportunidad para entender mejor cómo funciona el preprocesador hace su trabajo y qué reglas usa al expandir macros.
Entonces, a la luz de eso: ¿Cuál es el razonamiento detrás de la decisión del preprocesador de expandir la macro final a " EXPAND(0) " en lugar de " 0 "?
Editar: después de leer la respuesta muy detallada, lógica y bien expresada de Chris Dodd, hice lo que cualquiera haría en la misma situación ... intente encontrar un contraejemplo :)
Lo que inventé fue este diferente de 4 líneas:
#define EXPAND(X) X #define GLUE(X,Y) XY #define MACRO() GLUE(A,B) EXPAND(GLUE(MACRO, ())) // GLUE(A,B) Ahora, sabiendo el hecho de que el preprocesador C no está completo en Turing , no hay forma de que lo anterior se expanda a AB . Si ese fuera el caso, GLUE expandiría MACRO y MACRO expandiría GLUE . Eso conduciría a la posibilidad de una recursividad ilimitada, lo que probablemente implicaría la Completitud de Turing para el Cpp. Lamentablemente para los magos del preprocesador, la macro anterior que no se expande es una garantía.
Que fallar no es realmente el problema, el verdadero problema es: ¿Dónde? ¿Dónde decidió el preprocesador detener la expansión?
Analizando los pasos:
EXPAND y escanea en la lista de argumentos GLUE(MACRO, ()) para XGLUE(MACRO, ()) como una macro:MACRO y () como argumentosMACRO ()GLUE y escanea MACRO () en busca de macros, encontrando MACROGLUE(A,B)GLUE(A,B) en busca de macros y encuentra GLUE . Sin embargo, se suprime, por lo que se va como está.X después del paso 2 es GLUE(A,B) (observe que, dado que no estamos en el paso 4 de GLUE , en teoría, ya no se suprime)GLUE(A,B)EXPAND y escanea GLUE(A,B) en busca de más macros, encontrando GLUE ( uuh )A y B para los argumentos ( oh no )AB ( bueno... )AB en busca de macros, pero no encuentra nadaAB Cuál sería nuestro sueño. Lamentablemente, la macro se expande a GLUE(A,B) .
Así que nuestra pregunta es: ¿Por qué?
Después de leer la respuesta magistral de Chris Dodd y pasar un tiempo pensando, creo que he entendido el problema.
Si está utilizando el preprocesador C como una persona normal y cuerda , realmente no hay ningún problema que evitar aquí. Simplemente no hagas macros que se mencionen entre sí y estarás bien. Sin embargo, si está incursionando en las artes oscuras , encontrará que es sorprendentemente fácil tropezar con el problema anterior. Así que aquí, voy a explicar cómo evitarlo.
Tenga en cuenta que no voy a explicar por qué sucede esto aquí (la respuesta de Chris ya explica muy bien por qué), pero voy a exponer dónde suceden y cómo resolverlos .
El problema puede ocurrir cuando su macro "llamada" tiene una expansión pospuesta/indirecta . Lo que quiero decir con "indirecto" es una expansión que no se puede hacer en ese mismo momento, sino solo después de alguna concatenación o sustitución/reemplazo. Para entenderlo, veamos primero un ejemplo seguro:
#define EXPAND(MACRO) MACRO #define MY_MACRO(A, B) A##B EXPAND(MY_MACRO(1,2)) // 12 Aquí, MY_MACRO(1,2) es una referencia inmediata a una macro y tiene los argumentos adecuados para expandirse tal cual. Por lo tanto, el preprocesador lo expandirá inmediatamente , antes de cualquier reemplazo en EXPAND .
Ahora vamos a compararlo con estos ejemplos:
#define EXPAND(MACRO, PAREN_ARGS) MACRO PAREN_ARGS #define MY_MACRO(A, B) A##B EXPAND(MY_MACRO, (1,2)) // 12 #define EXPAND(MACRO) MACRO (1,2) #define MY_MACRO(A, B) A##B EXPAND(MY_MACRO) // 12 Tenga en cuenta que los argumentos dentro de EXPAND (" MY_MACRO, (1,2) " y " MY_MACRO ") no "parecen una macro". Aunque queremos que se expandan por completo al final, no tienen el formato correcto para expandirse en ese mismo momento del argumento. Aunque estos funcionan bien ahora, debido a que no hay una referencia mutua, su expansión se pospondrá después del reemplazo en EXPAND , y eso puede plantear un problema.
Siempre que la macro en su argumento no se pueda expandir tal como está, no puede contener el "destinatario" en ningún punto de su expansión .
Podemos mostrar esto agregando una referencia mutua a los ejemplos anteriores:
#define EXPAND(MACRO, PAREN_ARGS) MACRO PAREN_ARGS // Now MY_MACRO references EXPAND #define MY_MACRO(A, B) EXPAND(A,B) // Fails to expand EXPAND(MY_MACRO, (1,2)) // EXPAND(1,2) ^ ^ | | | This guy is postponed... | ...so it cannot eventually expand to this guy. #define EXPAND(MACRO) MACRO (1,2) // Now MY_MACRO references EXPAND #define MY_MACRO(A, B) EXPAND(A) // Fails to expand EXPAND(MY_MACRO) // EXPAND(1) ^ ^ | | | This guy is postponed... | ...so it cannot eventually expand to this guy.Nuestro ejemplo sin la expansión indirecta por otro lado, es completamente seguro y se evalúa como se esperaba:
#define EXPAND(MACRO) MACRO // Now MY_MACRO references EXPAND #define MY_MACRO(A, B) EXPAND(A) // Succeeds EXPAND(MY_MACRO(1,2)) // 1 ^ ^ | | | This guy can expand right here... | ...so it can freely expand to this guy. Dado que MY_MACRO(1,2) es una macro real, se puede evaluar en ese mismo momento y no se producen problemas.
Si mi juicio es correcto, no tiene que preocuparse por ningún "llamado anterior", solo el que directamente tiene como argumento la "macro pospuesta". Tampoco tiene que preocuparse por los otros argumentos, ya que el preprecesor también intentará expandirlos por completo antes del reemplazo. Además, esto no supone ningún problema si no se requiere la expansión completa de la macro.
Sin embargo, si desea que la macro en su argumento se expanda por completo y su expansión será indirecta, verifíquelo dos veces.
La expansión de macros es un proceso complejo que en realidad solo es comprensible al comprender los pasos que ocurren.
Cuando se reconoce una macro con argumentos (token de nombre de macro seguido de ( token), los siguientes tokens hasta el correspondiente ) se escanean y dividen (on , tokens). No ocurre ninguna expansión de macro mientras esto sucede (por lo que , s y ) deben estar presentes en el flujo de entrada directamente y no pueden estar en otras macros).
Cada argumento de macro cuyo nombre aparece en el cuerpo de la macro no precedido por # o ## o seguido por ## es "preescaneado" para que las macros se expandan; cualquier macro que esté completamente dentro del argumento se expandirá recursivamente antes de sustituirla en el cuerpo de la macro.
Los flujos de token de argumento de macro resultantes se sustituyen en el cuerpo de la macro. Los argumentos involucrados en las operaciones # o ## se modifican (en cadena o se pegan) y se sustituyen en función de los tokens del analizador original del paso 1 (el paso 2 no ocurre para estos).
El flujo de token del cuerpo de la macro resultante se escanea nuevamente en busca de macros para expandir, pero ignorando la macro que se está expandiendo actualmente. En este punto, se pueden incluir más tokens en la entrada (después de lo que se escaneó y analizó en el paso 1) como parte de cualquier macro reconocida.
Lo importante es que hay DOS DIFERENTES expansiones recursivas que ocurren (paso 2 y paso 4 arriba) y SOLO la del paso 4 ignora las expansiones de macro recursivas de la misma macro. La expansión recursiva en el paso 2 NO ignora la macro actual, por lo que puede expandirla recursivamente.
Entonces, para su ejemplo anterior, veamos qué sucede. para la entrada
EXPAND(TEST PARENTHESIS())EXPAND y escanea en la lista de argumentos TEST PARENTHESIS() para XTEST como una macro (sin seguir ( ), pero reconoce PARENTHESIS :() produciendo exactamente eso: ()() en busca de macros y no encuentra ningunaX después del paso 2 es TEST ()TEST ()EXPAND y escanea el resultado del paso 3 en busca de más macros, encontrando TESTEXPAND(0)TEST . En este punto, tanto EXPAND como TEST están suprimidos (debido a que se encuentran en el paso 4 de expansión), por lo que no sucede nada. Tu otro ejemplo EXPAND(TEST()) es diferente
EXPAND se reconoce como una macro, y TEST() se analiza como el argumento XEXPAND NO ESTÁ SUPRIMIDOTEST se reconoce como una macro con un argumento de secuencia vacíoEXPAND(0)TEST se suprime y el resultado se expande recursivamenteEXPAND se reconoce como una macro (recuerde, en este punto, solo TEST se suprime mediante la recursividad del paso 4; EXPAND está en la recursividad del paso 2, por lo que no se suprime) con 0 como argumento0 y no le pasa nada00 se escanea nuevamente en busca de macros (y nuevamente no sucede nada)0 se sustituye como argumento X en el cuerpo del primer EXPAND0 se escanea nuevamente en busca de macros (y nuevamente no sucede nada) entonces el resultado final aquí es 0
A los efectos de esta situación, hay tres pasos relevantes en el reemplazo de macros:
En EXPAND(TEST PARENTHESIS()) :
EXPAND , TEST PARENTHESIS() :TEST no va seguido de paréntesis, por lo que no se interpreta como una invocación de macro.PARENTHESIS() es una invocación de macro, por lo que se realizan los tres pasos: Los argumentos están vacíos, por lo que no hay procesamiento para ellos. Luego PARENTHESIS() se reemplaza por () . Luego () se vuelve a escanear y no se encuentran macros.EXPAND(TEST ()) . ( TEST () no se vuelve a escanear porque no fue el resultado de ningún reemplazo de macro).EXPAND(TEST ()) se reemplaza por TEST () .TEST () se vuelve a escanear mientras se suprime EXPAND :TEST () se reemplaza por EXPAND(0) .EXPAND(0) , pero se suprime EXPAND . En EXPAND(TEST ()) :
EXPAND :TEST están vacíos, por lo que no hay procesamiento.TEST () se reemplaza por EXPAND(0) .EXPAND(0) se reemplaza por 0 .EXPAND(TEST ()) se ha convertido en EXPAND(0) y EXPAND(0) se reemplaza por 0 .0 se vuelve a escanear en busca de más macros, pero no hay ninguna.Los otros ejemplos en la pregunta siguen de manera similar. Se reduce a:
TEST PARENTHESIS() , la falta de paréntesis después de TEST da como resultado que no se expanda mientras se procesan argumentos para una invocación de macro adjunta.PARENTHESIS , pero esto es después de escanear TEST y no se vuelve a escanear durante el procesamiento del argumento.TEST y luego se reemplaza, pero, en este momento, se suprime el nombre de la macro adjunta.