¿Cuáles son las aplicaciones del operador del preprocesador ## y las trampas a considerar?


88

Como mencioné en muchas de mis preguntas anteriores, estoy trabajando con K&R y actualmente estoy en el preprocesador. Una de las cosas más interesantes, algo que nunca supe antes de mis intentos anteriores de aprender C, es el ##operador del preprocesador. Según K&R:

El operador del preprocesador ## proporciona una forma de concatenar argumentos reales durante la expansión de macros. Si un parámetro en el texto de reemplazo es adyacente a a ##, el parámetro se reemplaza por el argumento real, el ##espacio en blanco y los alrededores se eliminan y el resultado se vuelve a escanear. Por ejemplo, la macro paste concatena sus dos argumentos:

#define paste(front, back) front ## back

entonces paste(name, 1)crea el token name1.

¿Cómo y por qué alguien usaría esto en el mundo real? ¿Cuáles son ejemplos prácticos de su uso y hay trampas a considerar?

Respuestas:


47

CrashRpt: uso de ## para convertir cadenas de macro de varios bytes a Unicode

Un uso interesante en CrashRpt (biblioteca de informes de fallos) es el siguiente:

#define WIDEN2(x) L ## x
#define WIDEN(x) WIDEN2(x)
//Note you need a WIDEN2 so that __DATE__ will evaluate first.

Aquí quieren usar una cadena de dos bytes en lugar de una cadena de un byte por carácter. Esto probablemente parece que no tiene sentido, pero lo hacen por una buena razón.

 std::wstring BuildDate = std::wstring(WIDEN(__DATE__)) + L" " + WIDEN(__TIME__);

Lo usan con otra macro que devuelve una cadena con la fecha y la hora.

Poner al Llado de un __ DATE __daría un error de compilación.


Windows: uso de ## para Unicode genérico o cadenas de varios bytes

Windows usa algo como lo siguiente:

#ifdef  _UNICODE
    #define _T(x)      L ## x
#else
    #define _T(x) x
#endif

Y _Tse usa en todas partes en el código.


Varias bibliotecas, que se utilizan para nombres limpios de accesos y modificadores:

También lo he visto usado en el código para definir accesores y modificadores:

#define MYLIB_ACCESSOR(name) (Get##name)
#define MYLIB_MODIFIER(name) (Set##name)

Asimismo, puede utilizar este mismo método para cualquier otro tipo de creación inteligente de nombres.


Varias bibliotecas, usándola para realizar varias declaraciones de variables a la vez:

#define CREATE_3_VARS(name) name##1, name##2, name##3
int CREATE_3_VARS(myInts);
myInts1 = 13;
myInts2 = 19;
myInts3 = 77;

3
Dado que puede concatenar cadenas literales en tiempo de compilación, puede reducir la expresión BuildDate std::wstring BuildDate = WIDEN(__DATE__) L" " WIDEN(__TIME__); y construir implícitamente toda la cadena a la vez.
user666412

49

Una cosa que debe tener en cuenta cuando usa los operadores de preprocesamiento token-paste (' ##') o stringizing (' #') es que debe usar un nivel adicional de indirección para que funcionen correctamente en todos los casos.

Si no hace esto y los elementos que se pasan al operador de pegado de tokens son macros en sí mismos, obtendrá resultados que probablemente no sean los que desea:

#include <stdio.h>

#define STRINGIFY2( x) #x
#define STRINGIFY(x) STRINGIFY2(x)
#define PASTE2( a, b) a##b
#define PASTE( a, b) PASTE2( a, b)

#define BAD_PASTE(x,y) x##y
#define BAD_STRINGIFY(x) #x

#define SOME_MACRO function_name

int main() 
{
    printf( "buggy results:\n");
    printf( "%s\n", STRINGIFY( BAD_PASTE( SOME_MACRO, __LINE__)));
    printf( "%s\n", BAD_STRINGIFY( BAD_PASTE( SOME_MACRO, __LINE__)));
    printf( "%s\n", BAD_STRINGIFY( PASTE( SOME_MACRO, __LINE__)));

    printf( "\n" "desired result:\n");
    printf( "%s\n", STRINGIFY( PASTE( SOME_MACRO, __LINE__)));
}

La salida:

buggy results:
SOME_MACRO__LINE__
BAD_PASTE( SOME_MACRO, __LINE__)
PASTE( SOME_MACRO, __LINE__)

desired result:
function_name21

1
Para obtener una explicación de este comportamiento del preprocesador, consulte stackoverflow.com/questions/8231966/…
Adam Davis

@MichaelBurr estaba leyendo tu respuesta y tengo una duda. ¿Cómo es que esta LÍNEA está imprimiendo el número de línea?
HELP PLZ

3
@AbhimanyuAryan: No estoy seguro de si esto es lo que está preguntando, pero __LINE__es un nombre de macro especial que es reemplazado por el preprocesador con el número de línea actual del archivo fuente.
Michael Burr

Sería genial si las especificaciones del idioma pudieran citarse / vincularse, como aquí
Antonio

14

Aquí hay un problema con el que me encontré al actualizar a una nueva versión de un compilador:

El uso innecesario del operador de pegado de tokens ( ##) no es portátil y puede generar espacios en blanco no deseados, advertencias o errores.

Cuando el resultado del operador de pegado de tokens no es un token de preprocesador válido, el operador de pegado de tokens es innecesario y posiblemente dañino.

Por ejemplo, uno podría intentar construir cadenas literales en tiempo de compilación usando el operador de pegar tokens:

#define STRINGIFY(x) #x
#define PLUS(a, b) STRINGIFY(a##+##b)
#define NS(a, b) STRINGIFY(a##::##b)
printf("%s %s\n", PLUS(1,2), NS(std,vector));

En algunos compiladores, esto generará el resultado esperado:

1+2 std::vector

En otros compiladores, esto incluirá espacios en blanco no deseados:

1 + 2 std :: vector

Las versiones bastante modernas de GCC (> = 3.3 más o menos) no compilarán este código:

foo.cpp:16:1: pasting "1" and "+" does not give a valid preprocessing token
foo.cpp:16:1: pasting "+" and "2" does not give a valid preprocessing token
foo.cpp:16:1: pasting "std" and "::" does not give a valid preprocessing token
foo.cpp:16:1: pasting "::" and "vector" does not give a valid preprocessing token

La solución es omitir el operador de pegado de tokens al concatenar tokens de preprocesador a operadores C / C ++:

#define STRINGIFY(x) #x
#define PLUS(a, b) STRINGIFY(a+b)
#define NS(a, b) STRINGIFY(a::b)
printf("%s %s\n", PLUS(1,2), NS(std,vector));

El capítulo de documentación de GCC CPP sobre concatenación tiene más información útil sobre el operador de pegado de tokens.


Gracias, no estaba al tanto de esto (pero entonces no uso demasiado estos operadores de preprocesamiento ...).
Michael Burr

3
Se llama operador de "pegado de tokens" por una razón: la intención es terminar con un solo token cuando haya terminado. Buen comentario.
Mark Ransom

Cuando el resultado del operador de pegado de tokens no es un token de preprocesador válido, el comportamiento no está definido.
alecov

Los cambios de lenguaje como flotantes hexadecimales, o (en C ++) separadores de dígitos y literales definidos por el usuario, cambian continuamente lo que constituye un "token de preprocesamiento válido", ¡así que nunca abuse de él de esa manera! Si tiene que separar tokens (en el idioma apropiado), deletree como dos tokens separados y no confíe en interacciones accidentales entre la gramática del preprocesador y el idioma en sí.
Kerrek SB

6

Esto es útil en todo tipo de situaciones para no repetirse innecesariamente. El siguiente es un ejemplo del código fuente de Emacs. Nos gustaría cargar una serie de funciones de una biblioteca. Debe asignarse la función "foo" fn_foo, y así sucesivamente. Definimos la siguiente macro:

#define LOAD_IMGLIB_FN(lib,func) {                                      \
    fn_##func = (void *) GetProcAddress (lib, #func);                   \
    if (!fn_##func) return 0;                                           \
  }

Entonces podemos usarlo:

LOAD_IMGLIB_FN (library, XpmFreeAttributes);
LOAD_IMGLIB_FN (library, XpmCreateImageFromBuffer);
LOAD_IMGLIB_FN (library, XpmReadFileToImage);
LOAD_IMGLIB_FN (library, XImageFree);

El beneficio es no tener que escribir ambos fn_XpmFreeAttributesy "XpmFreeAttributes"(y arriesgarse a escribir mal uno de ellos).


4

Una pregunta anterior en Stack Overflow pedía un método fluido para generar representaciones de cadenas para constantes de enumeración sin mucha reescritura propensa a errores.

Enlace

Mi respuesta a esa pregunta mostró cómo la aplicación de poca magia de preprocesador le permite definir su enumeración de esta manera (por ejemplo) ...;

ENUM_BEGIN( Color )
  ENUM(RED),
  ENUM(GREEN),
  ENUM(BLUE)
ENUM_END( Color )

... Con el beneficio de que la expansión de macro no solo define la enumeración (en un archivo .h), también define una matriz de cadenas coincidente (en un archivo .c);

const char *ColorStringTable[] =
{
  "RED",
  "GREEN",
  "BLUE"
};

El nombre de la tabla de cadenas proviene de pegar el parámetro macro (es decir, Color) en StringTable usando el operador ##. Aplicaciones (¿trucos?) Como esta son donde los operadores # y ## son invaluables.


3

Puede usar el pegado de tokens cuando necesite concatenar parámetros de macro con algo más.

Se puede utilizar para plantillas:

#define LINKED_LIST(A) struct list##_##A {\
A value; \
struct list##_##A *next; \
};

En este caso, LINKED_LIST (int) le daría

struct list_int {
int value;
struct list_int *next;
};

De manera similar, puede escribir una plantilla de función para el recorrido de la lista.


2

Lo uso en programas C para ayudar a hacer cumplir correctamente los prototipos para un conjunto de métodos que deben ajustarse a algún tipo de convención de llamadas. En cierto modo, esto se puede utilizar para la orientación de objetos del hombre pobre en C recta:

SCREEN_HANDLER( activeCall )

se expande a algo como esto:

STATUS activeCall_constructor( HANDLE *pInst )
STATUS activeCall_eventHandler( HANDLE *pInst, TOKEN *pEvent );
STATUS activeCall_destructor( HANDLE *pInst );

Esto aplica la parametrización correcta para todos los objetos "derivados" cuando lo hace:

SCREEN_HANDLER( activeCall )
SCREEN_HANDLER( ringingCall )
SCREEN_HANDLER( heldCall )

lo anterior en sus archivos de encabezado, etc. También es útil para el mantenimiento si incluso desea cambiar las definiciones y / o agregar métodos a los "objetos".


2

SGlib usa ## básicamente para modificar plantillas en C. Debido a que no hay sobrecarga de funciones, ## se usa para pegar el nombre del tipo en los nombres de las funciones generadas. Si tuviera un tipo de lista llamado list_t, obtendría funciones denominadas como sglib_list_t_concat, y así sucesivamente.


2

Lo uso para una aserción de inicio en un compilador C no estándar para incrustado:



#define ASSERT(exp) if(!(exp)){ \
                      print_to_rs232("Assert failed: " ## #exp );\
                      while(1){} //Let the watchdog kill us 



3
Supongo que quiere decir con 'no estándar' que el compilador no pegó cadenas pero sí pegó tokens, ¿o habría funcionado incluso sin él ##?
PJTraill

1

Lo uso para agregar prefijos personalizados a variables definidas por macros. Entonces algo como:

UNITTEST(test_name)

se expande a:

void __testframework_test_name ()

1

El uso principal es cuando tiene una convención de nomenclatura y desea que su macro aproveche esa convención de nomenclatura. Quizás tenga varias familias de métodos: image_create (), image_activate () e image_release () también file_create (), file_activate (), file_release () y mobile_create (), mobile_activate () y mobile_release ().

Podría escribir una macro para manejar el ciclo de vida del objeto:

#define LIFECYCLE(name, func) (struct name x = name##_create(); name##_activate(x); func(x); name##_release())

Por supuesto, una especie de "versión mínima de objetos" no es el único tipo de convención de nomenclatura a la que se aplica: casi la gran mayoría de las convenciones de nomenclatura utilizan una subcadena común para formar los nombres. Podría utilizar nombres de funciones (como arriba) o nombres de campos, nombres de variables o casi cualquier otra cosa.


1

Un uso importante en WinCE:

#define BITFMASK(bit_position) (((1U << (bit_position ## _WIDTH)) - 1) << (bit_position ## _LEFTSHIFT))

Al definir la descripción del bit de registro, hacemos lo siguiente:

#define ADDR_LEFTSHIFT                          0

#define ADDR_WIDTH                              7

Y mientras usa BITFMASK, simplemente use:

BITFMASK(ADDR)

0

Es muy útil para registrar. Tu puedes hacer:

#define LOG(msg) log_msg(__function__, ## msg)

O, si su compilador no admite function y func :

#define LOG(msg) log_msg(__file__, __line__, ## msg)

Las "funciones" anteriores registran el mensaje y muestran exactamente qué función registró un mensaje.

Es posible que mi sintaxis de C ++ no sea del todo correcta.


1
¿Qué estabas intentando hacer con eso? Funcionaría igual de bien sin el "##", ya que no hay necesidad de pegar token "," en "msg". ¿Estabas intentando secuenciar msg? Además, FILE y LINE deben estar en mayúsculas, no en minúsculas.
bk1e

De hecho tienes razón. Necesito encontrar el script original para ver cómo se usó ##. ¡Qué vergüenza, no hay galleta hoy!
ya23
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.