risorse | x macro
X Macro è una tecnica per generare sequenze ripetitive di codice a compile-time[3]. È piuttosto datata: risale infatti agli anni '60[2], epoca dei primi macro-assemblatori.
Un tipico esempio d'uso è l'attribuzione di un'etichetta agli elementi di un enumerato:
#include <iostream> enum Color { e_blue, e_red, e_green }; int main() { std::cout << e_blue << std::endl; std::cout << e_red << std::endl; std::cout << e_green << std::endl; return 0; } /* output: * * 0 * 1 * 2 */
Supponendo di voler associare ad ogni elemento dell'enumerato un'etichetta cui sia facile risalire a partire dall'elemento stesso, si può sfruttare un array di stringhe:
#include <iostream> #include <string> enum Color { e_blue, e_red, e_green }; static std::string color_names[] = { "Blue", "Red", "Green", }; int main() { std::cout << e_blue << ": " << color_names[e_blue ] << std::endl; std::cout << e_red << ": " << color_names[e_red ] << std::endl; std::cout << e_green << ": " << color_names[e_green] << std::endl; return 0; } /* output: * * 0: Blue * 1: Red * 2: Green */
Volendo rendere più esplicita la relazione tra gli elementi dell'enumerato e le relative etichette, si può pensare di sintetizzare la relazione in un unica dichiarazione, in una forma tale che sia facile ottenere in un secondo tempo i due elenchi. A tal fine si ricorre al pre-processore, introducendo una direttiva #define d'appoggio:
#define COLORS \ X(e_blue, "Blue" ) \ X(e_red, "Red" ) \ X(e_green, "Green")
L'espansione di COLORS dipende ovviamente dalla definizione di X; l'idea alla base della tecnica consiste nel fornire di volta in volta una versione di X differente, in funzione delle specifiche necessità. Ad esempio, una formulazione di X adatta alla dichiarazione dell'enumerato è la seguente:
#define X(name_, label_) name_, enum Color { COLORS }; #undef X /* expands to: * * enum Color { e_blue, e_red, e_green, }; */
Fornendo un'altra formulazione di X si ottiene la dichiarazione del vettore delle etichette:
#define X(name_, label_) label_, static std::string color_names[] = { COLORS }; #undef X /* expands to: * * static std::string color_names[] = { "Blue", "Red", "Green", }; */
La macro X viene dunque ridefinita in funzione dell'esigenza locale: estrazione del nome nel contesto nell'enumerazione Colors, estrazione dell'etichetta nella dichiarazione dell'array color_names. Diventa così molto più semplice e sicura la gestione dei colori, essendoci un unico punto di intervento, ovvero la dichiarazione COLORS. La versione completa dell'esempio diventa:
#include <iostream> #include <string> #define COLORS \ X(e_blue, "Blue" ) \ X(e_red, "Red" ) \ X(e_green, "Green") #define X(name_, label_) name_, enum Color { COLORS }; #undef X #define X(name_, label_) label_, static std::string color_names[] = { COLORS }; #undef X int main() { std::cout << e_blue << ": " << color_names[e_blue ] << std::endl; std::cout << e_red << ": " << color_names[e_red ] << std::endl; std::cout << e_green << ": " << color_names[e_green] << std::endl; return 0; } /* output: * * 0: Blue * 1: Red * 2: Green */
Se per qualche ragione non è possibile usare il simbolo X (per esempio perché già definito), si introduce un livello di indirezione:
#include <iostream> #include <string> #define PICK_NAME(name_, label_) e_ ## name_, #define PICK_LABEL(name_, label_) label_, #define COLORS(lambda_) \ lambda_(blue, "Blue" ) \ lambda_(red, "Red" ) \ lambda_(green, "Green") enum Color { COLORS(PICK_NAME) }; static std::string color_names[] = { COLORS(PICK_LABEL) }; int main() { std::cout << e_blue << ": " << color_names[e_blue ] << std::endl; std::cout << e_red << ": " << color_names[e_red ] << std::endl; std::cout << e_green << ": " << color_names[e_green] << std::endl; return 0; } /* output: * * 0: Blue * 1: Red * 2: Green */
Se l'etichetta coincide con l'identificatore dell'enumerato, la si può omettere:
#include <iostream> #include <string> #define PICK_NAME(name_) e_ ## name_, #define PICK_LABEL(name_) #name_, #define COLORS(lambda_) \ lambda_(blue ) \ lambda_(red ) \ lambda_(green) enum Color { COLORS(PICK_NAME) }; static std::string color_names[] = { COLORS(PICK_LABEL) }; int main() { std::cout << e_blue << ": " << color_names[e_blue ] << std::endl; std::cout << e_red << ": " << color_names[e_red ] << std::endl; std::cout << e_green << ": " << color_names[e_green] << std::endl; return 0; } /* output: * * 0: blue * 1: red * 2: green */
Se il compilatore in uso supporta le initializer list introdotte nel C++11 (es. MinGW 8.0), è facile attribuire agli elementi dell'enumerato un valore numerico arbitrario, sostituendo il vettore con una mappa (non essendo progressivi, i valori numerici dell'enumerato non possono più essere utilizzati come indici):
// does not compile on VC11 #include <iostream> #include <map> #include <string> #define PICK_NAME_AND_ID(name_, id_, label_) e_ ## name_ = id_, #define PICK_NAME_AND_LABEL(name_, id_, label_) { e_ ## name_, label_ }, #define COLORS(lambda_) \ lambda_(blue, 1, "Blue" ) \ lambda_(red, 2, "Red" ) \ lambda_(green, 4, "Green") enum Color { COLORS(PICK_NAME_AND_ID) }; static std::map<Color, std::string> color_names = { COLORS(PICK_NAME_AND_LABEL) }; int main() { std::cout << e_blue << ": " << color_names[e_blue ] << std::endl; std::cout << e_red << ": " << color_names[e_red ] << std::endl; std::cout << e_green << ": " << color_names[e_green] << std::endl; return 0; } /* output: * * 1: Blue * 2: Red * 4: Green */
L'esempio C++ basato su array è facilmente convertibile in C:
#include <stdio.h> #define PICK_NAME(name_, label_) e_ ## name_, #define PICK_LABEL(name_, label_) label_, #define COLORS(lambda_) \ lambda_(blue, "Blue" ) \ lambda_(red, "Red" ) \ lambda_(green, "Green") enum Color { COLORS(PICK_NAME) }; static const char* const color_names[] = { COLORS(PICK_LABEL) }; int main() { printf("%d: %s\n", e_blue, color_names[e_blue ]); printf("%d: %s\n", e_red, color_names[e_red ]); printf("%d: %s\n", e_green, color_names[e_green]); return 0; } /* output: * * 0: Blue * 1: Red * 2: Green */
Meno banale è la conversione dell'esempio basato sulla mappa, a meno di non possedere un compilatore C99-compliant (es. MinGW 8.0). Questo standard contempla infatti i designated initializers, che cosentono, tra altre cose, di inizializzare un sotto-insieme arbitrario di un array:
int a[6] = { [4] = 29, [2] = 15 }; /* same as: * * int a[6] = { 0, 0, 15, 0, 29, 0 }; */
Il codice d'esempio diventa in questo caso:
// does not compile on VC11 #include <stdio.h> #define PICK_NAME(name_, id_, label_) e_ ## name_ = id_, #define PICK_LABEL(name_, id_, label_) [e_ ## name_] = label_, #define COLORS(lambda_) \ lambda_(blue, 1, "Blue" ) \ lambda_(red, 2, "Red" ) \ lambda_(green, 4, "Green") enum Color { COLORS(PICK_NAME) }; static const char* color_names[] = { COLORS(PICK_LABEL) }; int main() { printf("%d: %s\n", e_blue, color_names[e_blue ]); printf("%d: %s\n", e_red, color_names[e_red ]); printf("%d: %s\n", e_green, color_names[e_green]); return 0; } /* output: * * 1: Blue * 2: Red * 4: Green */
Pagina modificata il 16/05/2013