Qual è un buon riferimento per la documentazione dei modelli di utilizzo di X-Macros in C (o forse in C ++)?

Una definizione e un esempio di base e alcuni riferimenti per ” X-Macros ” sono forniti in questa voce wikipedia sul pre-processore C :

Una X-Macro è un file di intestazione (comunemente usando un’estensione “.def” invece del tradizionale “.h”) che contiene un elenco di chiamate macro simili (che possono essere chiamate “macro componenti”).

Quali sono alcune buone fonti di informazione su come usare questa potente tecnica? Esistono librerie open source ben conosciute che utilizzano questo metodo?

Io uso X Macros () nel codice molto. Il valore deriva dall’aggiunta di nuovi dati solo alla “lista X” e non modifica alcun altro codice.

L’uso più comune di X Macros () è per associare il testo di errore con i codici di errore. Quando vengono aggiunti nuovi codici di errore, i programmatori devono ricordare di aggiungere il codice e il testo, in genere in luoghi separati. X Macro consente di aggiungere i nuovi dati di errore in un unico punto e di popolarli automaticamente ovunque sia necessario.

Sfortunatamente, i meccanismi utilizzano un sacco di magia pre-compilatore che può rendere il codice un po ‘difficile da leggere (ad esempio, la stringa che unisce con token1##token2 , la creazione di stringhe con #token ). Per questo motivo in genere spieghiamo cosa sta facendo la macro X nei commenti.

Ecco un esempio che utilizza i valori di errore / ritorno. Tutti i nuovi dati vengono aggiunti X_ERRORX_ERROR “. Nessuno degli altri codici deve essere modificato.

 /* * X Macro() data list * Format: Enum, Value, Text */ #define X_ERROR \ X(ERROR_NONE, 1, "Success") \ X(ERROR_SYNTAX, 5, "Invalid syntax") \ X(ERROR_RANGE, 8, "Out of range") /* * Build an array of error return values * eg {0,5,8} */ static int ErrorVal[] = { #define X(Enum,Val,Text) Val, X_ERROR #undef X }; /* * Build an array of error enum names * eg {"ERROR_NONE","ERROR_SYNTAX","ERROR_RANGE"} */ static char * ErrorEnum[] = { #define X(Enum,Val,Text) #Enum, X_ERROR #undef X }; /* * Build an array of error strings * eg {"Success","Invalid syntax","Out of range"} */ static char * ErrorText[] = { #define X(Enum,Val,Text) Text, X_ERROR #undef X }; /* * Create an enumerated list of error indexes * eg 0,1,2 */ enum { #define X(Enum,Val,Text) IDX_##Enum, X_ERROR #undef X IDX_MAX /* Array size */ }; void showErrorInfo(void) { int i; /* * Access the values */ for (i=0; i 

Puoi anche usare X Macros () per generare codice. Ad esempio, per verificare se un valore di errore è "noto", la Macro X può generare casi in un'istruzione switch:

  /* * Test validity of an error value * case ERROR_SUCCESS: * case ERROR_SYNTAX: * case ERROR_RANGE: */ switch(value) { #define X(Enum,Val,Text) case Val: X_ERROR #undef X printf("Error %d is ok\n",value); break; default: printf("Invalid error: %d\n",value); break; } 

Ho scoperto X-macros un paio di anni fa quando ho iniziato a utilizzare i puntatori di funzione nel mio codice. Sono un programmatore incorporato e utilizzo spesso le macchine a stati. Spesso scrivo codice come questo:

 /* declare an enumeration of state codes */ enum{ STATE0, STATE1, STATE2, ... , STATEX, NUM_STATES}; /* declare a table of function pointers */ p_func_t jumptable[NUM_STATES] = {func0, func1, func2, ... , funcX}; 

Il problema era che consideravo molto incline agli errori mantenere l’ordine della tabella dei puntatori delle funzioni in modo tale che corrispondesse all’ordine della mia enumerazione di stati.

Un mio amico mi ha fatto conoscere X-macros ed è stato come se una lampadina si fosse spenta nella mia testa. Seriamente, dove sei stato per tutta la mia vita x-macros!

Quindi ora definisco la seguente tabella:

 #define STATE_TABLE \ ENTRY(STATE0, func0) \ ENTRY(STATE1, func1) \ ENTRY(STATE2, func2) \ ... ENTRY(STATEX, funcX) \ 

E posso usarlo come segue:

 enum { #define ENTRY(a,b) a, STATE_TABLE #undef ENTRY NUM_STATES }; 

e

 p_func_t jumptable[NUM_STATES] = { #define ENTRY(a,b) b, STATE_TABLE #undef ENTRY }; 

come bonus, posso anche fare in modo che il pre-processore costruisca i miei prototipi di funzioni come segue:

 #define ENTRY(a,b) static void b(void); STATE_TABLE #undef ENTRY 

Un altro uso è quello di dichiarare e inizializzare i registri

 #define IO_ADDRESS_OFFSET (0x8000) #define REGISTER_TABLE\ ENTRY(reg0, IO_ADDRESS_OFFSET + 0, 0x11)\ ENTRY(reg1, IO_ADDRESS_OFFSET + 1, 0x55)\ ENTRY(reg2, IO_ADDRESS_OFFSET + 2, 0x1b)\ ... ENTRY(regX, IO_ADDRESS_OFFSET + X, 0x33)\ /* declare the registers (where _at_ is a compiler specific directive) */ #define ENTRY(a, b, c) volatile uint8_t a _at_ b: REGISTER_TABLE #undef ENTRY /* initialize registers */ #def ENTRY(a, b, c) a = c; REGISTER_TABLE #undef ENTRY 

Il mio utilizzo preferito è comunque quando si tratta di gestori di comunicazione

Per prima cosa creo una tabella delle comunicazioni, contenente ogni nome e codice di comando:

 #define COMMAND_TABLE \ ENTRY(RESERVED, reserved, 0x00) \ ENTRY(COMMAND1, command1, 0x01) \ ENTRY(COMMAND2, command2, 0x02) \ ... ENTRY(COMMANDX, commandX, 0x0X) \ 

Ho entrambi i nomi maiuscoli e minuscoli nella tabella, perché il maiuscolo verrà utilizzato per le enumerazioni e il minuscolo per i nomi delle funzioni.

Quindi definisco anche le strutture per ciascun comando per definire l’aspetto di ciascun comando:

 typedef struct {...}command1_cmd_t; typedef struct {...}command2_cmd_t; etc. 

Allo stesso modo, definisco le strutture per ciascuna risposta di comando:

 typedef struct {...}response1_resp_t; typedef struct {...}response2_resp_t; etc. 

Quindi posso definire la mia enumerazione del codice comando:

 enum { #define ENTRY(a,b,c) a##_CMD = c, COMMAND_TABLE #undef ENTRY }; 

Posso definire la mia enumerazione di lunghezza di comando:

 enum { #define ENTRY(a,b,c) a##_CMD_LENGTH = sizeof(b##_cmd_t); COMMAND_TABLE #undef ENTRY }; 

Posso definire la mia lunghezza di risposta enumerazione:

 enum { #define ENTRY(a,b,c) a##_RESP_LENGTH = sizeof(b##_resp_t); COMMAND_TABLE #undef ENTRY }; 

Posso determinare quanti comandi ci sono come segue:

 typedef struct { #define ENTRY(a,b,c) uint8_t b; COMMAND_TABLE #undef ENTRY } offset_struct_t; #define NUMBER_OF_COMMANDS sizeof(offset_struct_t) 

NOTA: Non ho mai istanziato l’offset_struct_t, lo uso come un modo per il compilatore di generare il mio numero di comandi.

Nota quindi posso generare la mia tabella dei puntatori di funzione come segue:

 p_func_t jump_table[NUMBER_OF_COMMANDS] = { #define ENTRY(a,b,c) process_##b, COMMAND_TABLE #undef ENTRY } 

E i miei prototipi di funzioni:

 #define ENTRY(a,b,c) void process_##b(void); COMMAND_TABLE #undef ENTRY 

Ora, infine, per il miglior utilizzo ansible, posso fare in modo che il compilatore calcoli quanto dovrebbe essere grande il mio buffer di trasmissione.

 /* reminder the sizeof a union is the size of its largest member */ typedef union { #define ENTRY(a,b,c) uint8_t b##_buf[sizeof(b##_cmd_t)]; COMMAND_TABLE #undef ENTRY }tx_buf_t 

Ancora una volta questa unione è come la mia struct offset, non è istanziata, invece posso usare l’operatore sizeof per dichiarare la dimensione del mio buffer di trasmissione.

 uint8_t tx_buf[sizeof(tx_buf_t)]; 

Ora il mio buffer di trasmissione tx_buf è la dimensione ottimale e mentre aggiungo comandi a questo gestore di comunicazioni, il mio buffer sarà sempre la dimensione ottimale. Freddo!

Il Dr. Dobb’s ha un articolo su questo.