web-dev-qa-db-fra.com

Utilisation réelle des macros X

Je viens d'apprendre de X-Macros . Quelles utilisations réelles des macros X avez-vous vues? Quand sont-ils le bon outil pour le travail?

67

J'ai découvert les macros X il y a quelques années lorsque j'ai commencé à utiliser des pointeurs de fonction dans mon code. Je suis un programmeur intégré et j'utilise fréquemment des machines à états. Souvent, j'écrivais du code comme ceci:

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

Le problème était que je considérais comme très sujet aux erreurs de devoir maintenir l'ordre de ma table de pointeurs de fonction de telle sorte qu'il corresponde à l'ordre de mon énumération d'états.

Un de mes amis m'a présenté les macros X et c'était comme si une ampoule avait explosé dans ma tête. Sérieusement, où avez-vous été toute ma vie x-macros!

Alors maintenant, je définis le tableau suivant:

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

Et je peux l'utiliser comme suit:

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

et

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

en prime, je peux aussi demander au préprocesseur de construire mes prototypes de fonction comme suit:

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

Une autre utilisation est de déclarer et d'initialiser des registres

#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 */
#define ENTRY(a, b, c) a = c;
    REGISTER_TABLE
#undef ENTRY

Cependant, mon utilisation préférée concerne les gestionnaires de communication

Je crée d'abord une table de communications contenant chaque nom et code de commande:

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

J'ai les noms en majuscules et en minuscules dans le tableau, car les majuscules seront utilisées pour les énumérations et les minuscules pour les noms de fonctions.

Ensuite, je définis également des structures pour chaque commande afin de définir à quoi ressemble chaque commande:

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

etc.

De même, je définis des structures pour chaque réponse de commande:

typedef struct {...}command1_resp_t;
typedef struct {...}command2_resp_t;

etc.

Ensuite, je peux définir l'énumération de mon code de commande:

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

Je peux définir l'énumération de la longueur de ma commande:

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

Je peux définir l'énumération de la longueur de ma réponse:

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

Je peux déterminer le nombre de commandes comme suit:

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)

REMARQUE: je n'ai jamais réellement instancié le offset_struct_t, je l'utilise simplement comme un moyen pour le compilateur de générer pour moi mon nombre de définition de commandes.

Notez que je peux générer ma table de pointeurs de fonction comme suit:

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

Et mes prototypes de fonction:

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

Enfin, pour l'utilisation la plus cool de ma vie, je peux demander au compilateur de calculer la taille de mon tampon de transmission.

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

Encore une fois, cette union est comme ma structure de décalage, elle n'est pas instanciée, au lieu de cela, je peux utiliser l'opérateur sizeof pour déclarer ma taille de tampon de transmission.

uint8_t tx_buf[sizeof(tx_buf_t)];

Maintenant, mon tampon de transmission tx_buf a la taille optimale et lorsque j'ajoute des commandes à ce gestionnaire de communications, mon tampon sera toujours de la taille optimale. Cool!

Une autre utilisation consiste à créer des tables de décalage: la mémoire étant souvent une contrainte sur les systèmes embarqués, je ne veux pas utiliser 512 octets pour ma table de saut (2 octets par pointeur X 256 commandes possibles) quand il s'agit d'un tableau clairsemé. Au lieu de cela, j'aurai un tableau des décalages 8 bits pour chaque commande possible. Ce décalage est ensuite utilisé pour indexer dans ma table de saut réelle qui ne doit plus être que NUM_COMMANDS * sizeof (pointeur). Dans mon cas avec 10 commandes définies. Ma table de saut est longue de 20 octets et j'ai une table décalée de 256 octets, soit un total de 276 octets au lieu de 512 octets. J'appelle ensuite mes fonctions comme suit:

jump_table[offset_table[command]]();

au lieu de

jump_table[command]();

Je peux créer une table offset comme ceci:

/* initialize every offset to 0 */
static uint8_t offset_table[256] = {0};

/* for each valid command, initialize the corresponding offset */
#define ENTRY(a,b,c) offset_table[c] = offsetof(offset_struct_t, b);
    COMMAND_TABLE
#undef ENTRY

où offsetof est une macro de bibliothèque standard définie dans "stddef.h"

Comme avantage secondaire, il existe un moyen très simple de déterminer si un code de commande est pris en charge ou non:

bool command_is_valid(uint8_t command)
{
    /* return false if not valid, or true (non 0) if valid */
    return offset_table[command];
}

C'est aussi pourquoi dans ma COMMAND_TABLE j'ai réservé l'octet de commande 0. Je peux créer une fonction appelée "process_reserved ()" qui sera appelée si un octet de commande invalide est utilisé pour indexer dans ma table de décalage.

88
ACRL

Les macros X sont essentiellement des modèles paramétrés. Ils sont donc le bon outil pour le travail si vous avez besoin de plusieurs choses similaires sous plusieurs formes. Ils vous permettent de créer une forme abstraite et de l'instancier selon différentes règles.

J'utilise des macros X pour produire des valeurs d'énumération sous forme de chaînes. Et depuis le rencontrer, je préfère fortement cette forme qui prend une macro "utilisateur" à appliquer à chaque élément. L'inclusion de plusieurs fichiers est beaucoup plus pénible à travailler.

/* x-macro constructors for error and type
   enums and string tables */
#define AS_BARE(a) a ,
#define AS_STR(a) #a ,

#define ERRORS(_) \
    _(noerror) \
    _(dictfull) _(dictstackoverflow) _(dictstackunderflow) \
    _(execstackoverflow) _(execstackunderflow) _(limitcheck) \
    _(VMerror)
enum err { ERRORS(AS_BARE) };
char *errorname[] = { ERRORS(AS_STR) };
/* puts(errorname[(enum err)limitcheck]); */

Je les utilise également pour la répartition des fonctions en fonction du type d'objet. Encore une fois en détournant la même macro que j'ai utilisée pour créer les valeurs d'énumération.

#define TYPES(_) \
    _(invalid) \
    _(null) \
    _(mark) \
    _(integer) \
    _(real) \
    _(array) \
    _(dict) \
    _(save) \
    _(name) \
    _(string) \
/*enddef TYPES */

#define AS_TYPE(_) _ ## type ,
enum { TYPES(AS_TYPE) };

L'utilisation de la macro garantit que tous mes indices de tableau correspondront aux valeurs d'énumération associées, car ils construisent leurs différentes formes en utilisant les jetons nus de la définition de macro (la macro TYPES).

typedef void evalfunc(context *ctx);

void evalquit(context *ctx) { ++ctx->quit; }

void evalpop(context *ctx) { (void)pop(ctx->lo, adrent(ctx->lo, OS)); }

void evalpush(context *ctx) {
    Push(ctx->lo, adrent(ctx->lo, OS),
            pop(ctx->lo, adrent(ctx->lo, ES)));
}

evalfunc *evalinvalid = evalquit;
evalfunc *evalmark = evalpop;
evalfunc *evalnull = evalpop;
evalfunc *evalinteger = evalpush;
evalfunc *evalreal = evalpush;
evalfunc *evalsave = evalpush;
evalfunc *evaldict = evalpush;
evalfunc *evalstring = evalpush;
evalfunc *evalname = evalpush;

evalfunc *evaltype[stringtype/*last type in enum*/+1];
#define AS_EVALINIT(_) evaltype[_ ## type] = eval ## _ ;
void initevaltype(void) {
    TYPES(AS_EVALINIT)
}

void eval(context *ctx) {
    unsigned ades = adrent(ctx->lo, ES);
    object t = top(ctx->lo, ades, 0);
    if ( isx(t) ) /* if executable */
        evaltype[type(t)](ctx);  /* <--- the payoff is this line here! */
    else
        evalpush(ctx);
}

L'utilisation de macros X de cette manière aide en fait le compilateur à donner des messages d'erreur utiles. J'ai omis la fonction evalarray de ce qui précède car cela détournerait de mon point de vue. Mais si vous essayez de compiler le code ci-dessus (commentant les autres appels de fonction et fournissant un typedef factice pour le contexte, bien sûr), le compilateur se plaindra d'une fonction manquante. Pour chaque nouveau type que j'ajoute, je me rappelle d'ajouter un gestionnaire lorsque je recompile ce module. Ainsi, la macro X permet de garantir que les structures parallèles restent intactes même pendant la croissance du projet.

Modifier:

Cette réponse a élevé ma réputation de 50%. Voici donc un peu plus. Ce qui suit est un exemple négatif, répondant à la question: quand pas pour utiliser les macros X?

Cet exemple montre l'empaquetage de fragments de code arbitraires dans le "record" X-. J'ai finalement abandonné cette branche du projet et n'ai pas utilisé cette stratégie dans les conceptions ultérieures (et pas faute d'essayer). Il est devenu peu sauvage, en quelque sorte. En effet la macro s'appelle X6 car à un moment donné il y avait 6 arguments, mais je me suis lassé de changer le nom de la macro.

/* Object types */
/* "'X'" macros for Object type definitions, declarations and initializers */
// a                      b            c              d
// enum,                  string,      union member,  printf d
#define OBJECT_TYPES \
X6(    nulltype,        "null",     int dummy      ,            ("<null>")) \
X6(    marktype,        "mark",     int dummy2      ,           ("<mark>")) \
X6( integertype,     "integer",     int  i,     ("%d",o.i)) \
X6( booleantype,     "boolean",     bool b,     (o.b?"true":"false")) \
X6(    realtype,        "real",     float f,        ("%f",o.f)) \
X6(    nametype,        "name",     int  n,     ("%s%s", \
        (o.flags & Fxflag)?"":"/", names[o.n])) \
X6(  stringtype,      "string",     char *s,        ("%s",o.s)) \
X6(    filetype,        "file",     FILE *file,     ("<file %p>",(void *)o.file)) \
X6(   arraytype,       "array",     Object *a,      ("<array %u>",o.length)) \
X6(    dicttype,        "dict",     struct s_pair *d, ("<dict %u>",o.length)) \
X6(operatortype,    "operator",     void (*o)(),    ("<op>")) \

#define X6(a, b, c, d) #a,
char *typestring[] = { OBJECT_TYPES };
#undef X6

// the Object type
//forward reference so s_object can contain s_objects
typedef struct s_object Object;

// the s_object structure:
// a bit convoluted, but it boils down to four members:
// type, flags, length, and payload (union of type-specific data)
// the first named union member is integer, so a simple literal object
// can be created on the fly:
// Object o = {integertype,0,0,4028}; //create an int object, value: 4028
// Object nl = {nulltype,0,0,0};
struct s_object {
#define X6(a, b, c, d) a,
    enum e_type { OBJECT_TYPES } type;
#undef X6
unsigned int flags;
#define Fread  1
#define Fwrite 2
#define Fexec  4
#define Fxflag 8
size_t length; //for lint, was: unsigned int
#define X6(a, b, c, d) c;
    union { OBJECT_TYPES };
#undef X6
};

Un gros problème était les chaînes de format printf. Bien que cela ait l'air cool, c'est juste un pocus hocus. Puisqu'il n'est utilisé que dans une seule fonction, la surutilisation de la macro a en fait séparé les informations qui devraient être ensemble; et il rend la fonction illisible par elle-même. L'obfuscation est doublement malheureuse dans une fonction de débogage comme celle-ci.

//print the object using the type's format specifier from the macro
//used by O_equal (ps: =) and O_equalequal (ps: ==)
void printobject(Object o) {
    switch (o.type) {
#define X6(a, b, c, d) \
        case a: printf d; break;
OBJECT_TYPES
#undef X6
    }
}

Alors ne vous laissez pas emporter. Comme j'ai fait.

35
luser droog

Dans la machine virtuelle Oracle HotSpot pour le langage de programmation Java®, il y a le fichier globals.hpp, qui utilise le RUNTIME_FLAGS de cette façon.

Voir le code source:

7
Roland Illig

J'aime utiliser les macros X pour créer des `` énumérations riches '' qui prennent en charge l'itération des valeurs d'énumération ainsi que l'obtention de la représentation sous forme de chaîne pour chaque valeur d'énumération:

#define MOUSE_BUTTONS \
X(LeftButton, 1)   \
X(MiddleButton, 2) \
X(RightButton, 4)

struct MouseButton {
  enum Value {
    None = 0
#define X(name, value) ,name = value
MOUSE_BUTTONS
#undef X
  };

  static const int *values() {
    static const int a[] = {
      None,
#define X(name, value) name,
    MOUSE_BUTTONS
#undef X
      -1
    };
    return a;
  }

  static const char *valueAsString( Value v ) {
#define X(name, value) static const char str_##name[] = #name;
MOUSE_BUTTONS
#undef X
    switch ( v ) {
      case None: return "None";
#define X(name, value) case name: return str_##name;
MOUSE_BUTTONS
#undef X
    }
    return 0;
  }
};

Cela définit non seulement un MouseButton::Value enum, ça me permet aussi de faire des choses comme

// Print names of all supported mouse buttons
for ( const int *mb = MouseButton::values(); *mb != -1; ++mb ) {
    std::cout << MouseButton::valueAsString( (MouseButton::Value)*mb ) << "\n";
}
5
Frerich Raabe

J'utilise une macro X assez massive pour charger le contenu du fichier INI dans une structure de configuration, entre autres choses tournant autour de cette structure.

Voici à quoi ressemble mon fichier "configuration.def":

#define NMB_DUMMY(...) X(__VA_ARGS__)
#define NMB_INT_DEFS \
   TEXT("long int") , long , , , GetLongValue , _ttol , NMB_SECT , SetLongValue , 

#define NMB_STR_DEFS NMB_STR_DEFS__(TEXT("string"))
#define NMB_PATH_DEFS NMB_STR_DEFS__(TEXT("path"))

#define NMB_STR_DEFS__(ATYPE) \
  ATYPE ,  basic_string<TCHAR>* , new basic_string<TCHAR>\
  , delete , GetValue , , NMB_SECT , SetValue , *

/* X-macro starts here */

#define NMB_SECT "server"
NMB_DUMMY(ip,TEXT("Slave IP."),TEXT("10.11.180.102"),NMB_STR_DEFS)
NMB_DUMMY(port,TEXT("Slave portti."),TEXT("502"),NMB_STR_DEFS)
NMB_DUMMY(slaveid,TEXT("Slave protocol ID."),0xff,NMB_INT_DEFS)
.
. /* And so on for about 40 items. */

C'est un peu déroutant, je l'avoue. Il devient rapidement clair que je ne veux pas vraiment écrire toutes ces déclarations de type après chaque macro de champ. (Ne vous inquiétez pas, il y a un gros commentaire pour expliquer tout ce que j'ai omis par souci de concision.)

Et voici comment je déclare la structure de configuration:

typedef struct {
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,...) TYPE ID;
#include "configuration.def"
#undef X
  basic_string<TCHAR>* ini_path;  //Where all the other stuff gets read.
  long verbosity;                 //Used only by console writing functions.
} Config;

Ensuite, dans le code, les valeurs par défaut sont d'abord lues dans la structure de configuration:

#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,SETTER,...) \
  conf->ID = CONSTRUCTOR(DEFVAL);
#include "configuration.def"
#undef X

Ensuite, le INI est lu dans la structure de configuration comme suit, en utilisant la bibliothèque SimpleIni:

#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,SETTER,DEREF...)\
  DESTRUCTOR (conf->ID);\
  conf->ID  = CONSTRUCTOR( ini.GETTER(TEXT(SECT),TEXT(#ID),DEFVAL,FALSE) );\
  LOG3A(<< left << setw(13) << TEXT(#ID) << TEXT(": ")  << left << setw(30)\
    << DEREF conf->ID << TEXT(" (") << DEFVAL << TEXT(").") );
#include "configuration.def"
#undef X

Et les remplacements à partir des drapeaux de ligne de commande, qui sont également formatés avec les mêmes noms (au format long GNU)), sont appliqués comme suit de la manière suivante en utilisant la bibliothèque SimpleOpt:

enum optflags {
#define X(ID,...) ID,
#include "configuration.def"
#undef X
  };
  CSimpleOpt::SOption sopt[] = {
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,...) {ID,TEXT("--") #ID TEXT("="), SO_REQ_CMB},
#include "configuration.def"
#undef X
    SO_END_OF_OPTIONS
  };
  CSimpleOpt ops(argc,argv,sopt,SO_O_NOERR);
  while(ops.Next()){
    switch(ops.OptionId()){
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,...) \
  case ID:\
    DESTRUCTOR (conf->ID);\
    conf->ID = STRCONV( CONSTRUCTOR (  ops.OptionArg() ) );\
    LOG3A(<< TEXT("Omitted ")<<left<<setw(13)<<TEXT(#ID)<<TEXT(" : ")<<conf->ID<<TEXT(" ."));\
    break;
#include "configuration.def"
#undef X
    }
  }

Et ainsi de suite, j'utilise également la même macro pour imprimer la sortie --help -flag et un exemple de fichier ini par défaut, configuration.def est inclus 8 fois dans mon programme. "Cheville carrée dans un trou rond", peut-être; comment un programmeur réellement compétent procéderait-il? Beaucoup, beaucoup de boucles et de traitement de chaînes?

4
VITTUIX-MAN

https://github.com/whunmr/DataEx

J'utilise les xmacros suivants pour générer une classe C++, avec des fonctionnalités de sérialisation et de désérialisation intégrées.

#define __FIELDS_OF_DataWithNested(_)  \
  _(1, a, int  )                       \
  _(2, x, DataX)                       \
  _(3, b, int  )                       \
  _(4, c, char )                       \
  _(5, d, __array(char, 3))            \
  _(6, e, string)                      \
  _(7, f, bool)

DEF_DATA(DataWithNested);

Usage:

TEST_F(t, DataWithNested_should_able_to_encode_struct_with_nested_struct) {
    DataWithNested xn;
    xn.a = 0xCAFEBABE;
    xn.x.a = 0x12345678;
    xn.x.b = 0x11223344;
    xn.b = 0xDEADBEEF;
    xn.c = 0x45;
    memcpy(&xn.d, "XYZ", strlen("XYZ"));

    char buf_with_zero[] = {0x11, 0x22, 0x00, 0x00, 0x33};
    xn.e = string(buf_with_zero, sizeof(buf_with_zero));
    xn.f = true;

    __encode(DataWithNested, xn, buf_);

    char expected[] = { 0x01, 0x04, 0x00, 0xBE, 0xBA, 0xFE, 0xCA,
                        0x02, 0x0E, 0x00 /*T and L of nested X*/,
                        0x01, 0x04, 0x00, 0x78, 0x56, 0x34, 0x12,
                        0x02, 0x04, 0x00, 0x44, 0x33, 0x22, 0x11,
                        0x03, 0x04, 0x00, 0xEF, 0xBE, 0xAD, 0xDE,
                        0x04, 0x01, 0x00, 0x45,
                        0x05, 0x03, 0x00, 'X', 'Y', 'Z',
                        0x06, 0x05, 0x00, 0x11, 0x22, 0x00, 0x00, 0x33,
                        0x07, 0x01, 0x00, 0x01};

    EXPECT_TRUE(ArraysMatch(expected, buf_));
}

En outre, un autre exemple se trouve dans https://github.com/whunmr/msgrpc .

1
whunmr