Random IRC quote :      <madalenasbuenas> yo os puedo brindar ese servicio debido a mi larga trayectoria como ladron de bancos, corresponsal de guerra <madalenasbuenas> jefe de ventas del circulo de lectores para oriente medio<madalenasbuenas> y farmeceutico ocasional

El extraño caso de la almohadilla asesina

almohadilla asesina
Buenas tardes a todos y todas

Hoy voy a comentar un pequeño problema con el que me he encontrado en el trabajo hace poco, y que ha dado lugar a un encendido debate sobre cual es el comportamiento esperado y el deseado del preprocesador de C ante una situacion un tanto peculiar.

Normalmente, como programador, suelo usar el VC++. Y no me refiero exclusivamente al ambito profesional, puesto que actualmente en casa estoy usando la version express del visual studio 2008 y el WDK (Para los despistados, la version 7.0.0 del WDK esta disponible desde hace poco mas de una semana). La interfaz me parece comoda y amigable, y el compilador es de una gran calidad.

Para el codigo que debia compilar tambien en linux estaba usando versiones del gcc relativamente viejas, la mas reciente de las cuales era una 2.9.x. Recientemente he comenzado a usar versiones 4.0.x. La verdad es que con este cambio esperaba eliminar algunos problemas de versiones antiguas del gcc (i.e. Soporte para __stdcall, soporte para uniones sin nombre, etc.) y en cambio no esperaba ver aparecer ningun nuevo impedimento. Pero nada mas lejos de la realidad. Aunque la mayoria de los problemas que tenia efectivamente se han solucionado, este compilador falla en la expansion de una macro ampliamente utilizada.

Un ejemplo muy simplificado del problema es el siguiente:

#define R1(a,b) a##b
#define R(a,b) R1(a,b)##2

int main(int argc, char*argv[])
{
  int a12;

  R(a,1) = 1;
  return 0;
}
 

En el ejemplo se espera que R(a,1) se expanda convirtiendose en «a12». Ese programa compila correctamente usando el VC++, y compilaba correctamente con versiones antiguas del gcc, pero he aqui que con las nuevas versiones del gcc la expansion de la macro introduce un espacio en blanco generando «a1 2». Para entender los problemas derivados del uso de las macros frikis es importante tener en cuenta que aunque usemos cadenas de texto para la explicacion, estas fases de traslacion del codigo se hace en base a Tokens del preprocesador.

Lo que hace el operador ## es generar un token uniendo los tokens que dicho operador tiene a su derecha y a su izquierda. El operador ## solo se aplica en el cuerpo de las macros, y ademas se aplica antes del reanalisis en busqueda de nuevas macros. Segun el estandar de C (Siempre que hablo del estandar de C me refiero al C90, no al C99) si la union de los dos tokens gracias al operador ## genera un token invalido, el comportamiento no queda definido por el estandar. O lo que es lo mismo, que cada compilador puede hacer lo que le de la gana.

Fijandonos en el ejemplo de arriba, podemos apreciar que el problema se encuentra en la macro R, donde el operador ## intenta crear el token «)2» que es claramente un invalido. El comportamiento del VC++ es evaluar R1(a,b) y construir posteriomente el token correcto. En cambio, el gcc 4.0.x decide ignorar el operaror, devolviendo un error e insertando un espacio en blanco.

Una vez que tenemos el problema claro, hay que buscar una solucion, pero creo que tambien se esta abriedo un debate interesante. ¿Cual de los dos comportationalmientso es el mas correcto?

Por de pronto, el gcc plantea un problema. Aplicaciones que con versiones antiguas compilaban correctamente, ahora no lo hacen. Despues de leer la documentacion del gcc, parece que no existe un modo de compatibilidad. Codigo historico como el famoso westley.c ya no compila con el gcc!!!! En principio podria parecer que la opcion -traditional deberia servir a tal efecto, pero lo cierto es que esa opcion solo se usa para codigo pre-estandar en el que el operador ## ni siquiera existia. Una curiosidad intersante es que en el c pre-estandar los comentarios funcionaban de una forma ligeramente parecida a como lo hace hoy dia el operador ## (Daros cuenta que en ansi-c el preprocesador sustituye los comentarios por un espacio en blanco, por lo que el funcionamiento es totalmente diferente). De esta manera el siguiente codigo:

#define R1(a,b) a/**/b
#define R(a,b) R1(a,b)/**/2

int main(int argc, char*argv[])
{
  int a12;

  R(a,1) = 1;
  return 0;
}
 

funciona correctamente cuando compilamos con «gcc -traditional -E riau.c > riau.i» generando el fichero preprocesado esperado. Pero lo cierto es que no he encontrado forma alguna de compilar el primero de los ejemplos.

Ahora bien, ¿Cual de los dos comportamientos es el mas acertado? ¿El del gcc 4.0.x? ¿El del VC++? Yo me decanto por el comportamiento del gcc. Aunque el VC++ parece tener un comportamiento ventajoso, lo cierto es que dudo mucho que eso sea cierto. En este caso concreto puede que sea una ventaja, pero existen otros casos en los que podria ser una desventaja. Daros cuenta que en el caso del gcc, siempre se va a comportar igual. Sin embargo en el caso del VC++ dependiendo de los defines previos nos podriamos encontrar con una yuxtaposicion de tokens o con una concatenacion de tokens, haciendo que el comportamiento dependa del contexto. De todas formas, el debate esta abierto. Aunque me decanto por una de las opciones, estoy dispuesto a dejarme convencer. Eso si, opondre resistencia :o)

En cuanto a cual es la solucion a este problema… Se me ocurren varias posibilidades, pero la que mas me gusta es la siguiente:

#define R1(a,b) a##b

#define _R(a) a##2
#define __R(a) _R(a)

#define R(a,b)  __R(R1(a,b))

int main(int argc, char*argv[])
{
  int a12;

  R(a,1) = 1;
  return 0;
}
 

La solucion consiste es definir una macro intermedia __R que obliga a que su parametro se evalue, de forma que al usar _R para concatenar, en el lado izquierdo ya tenemos el token que nos inateresa.

Espero que hayais disfrutado de la lectura!!!
Ahora vas y lo cascas!!!!!!!!

5 Comentarios para “El extraño caso de la almohadilla asesina”

  1. Comment por raimon | 08/22/09 at 4:29 am

    Muy interesante!! ¿para qué sirve?

  2. Comment por javi | 08/23/09 at 6:08 am

    hola,

    yo creo que según c90 no debería resolverse R1 para luego calcular la cadena compuesta con R. En todo caso debería componerse una cadena «R1(a,b)2».

    La mejor manera para hacer eso pienso que sería esto:

    #define R1 (a, b) a##b

    #define R(a,b) R1(a,b) «»##»2»

    R(«pepe»,»pepa») -> «pepepepa» «2»

    R1 ahí si se debería resolver bien, quedando la cadena delante «pepepepa» y detrás «2». En c90 dos cadenas seguidas se tratan como una única.

  3. Comment por javi | 08/23/09 at 6:15 am

    perdón, así quería decir:

    #define R1 (a, b) a##b

    #define R(a,b) R1(a,b) #2

  4. Comment por javi | 08/23/09 at 6:26 am

    Mmmm esto que digo yo es solo para cadenas claro… no he dicho nada! xD

  5. Comment por chema | 08/30/09 at 6:24 pm

Se han cerrado los comentarios