risorse | x macro

X Macro

Introduzione

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.

Esempio (C++)

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
 */

Generalizzazione

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
 */

Ricavare l'etichetta dall'identificatore

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
 */

Identificatori con valori arbitrari

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
 */

Esempio (C)

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
 */

Riferimenti

  1. Bright, W. "The X Macro". Dr.Dobb's. <http://www.drdobbs.com/cpp/the-x-macro/228700289>. Visitato il 15 maggio 2013.
  2. Meyers, R. "The New C: X Macros". Dr.Dobb's. <http://www.drdobbs.com/the-new-c-x-macros/184401387>. Visitato il 15 maggio 2013.
  3. "X Macro". Wikipedia. <http://en.wikipedia.org/wiki/X_Macro>. Visitato il 15 maggio 2013.
  4. "X-Macros". C Programming. <http://en.wikibooks.org/wiki/C_Programming/Preprocessor#X-Macros>. Visitato il 15 maggio 2013.

Pagina modificata il 16/05/2013