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?
Réponses
Trop de publicités?J'ai découvert X-macros il y a quelques années quand j'ai commencé à faire usage de pointeurs de fonction dans mon code. Je suis un intégré à l'programmeur et j'utilise l'état des machines fréquemment. Souvent je voudrais écrire un 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 est que je considère que c'est très sujettes à erreur d'avoir à maintenir la commande de mon pointeur de fonction tableau de manière à ce qu'il correspondait à la commande de mon énumération des états.
Un de mes amis m'a présenté à X-macros et c'était comme une ampoule a explosé dans ma tête. Sérieusement, où avez-vous été toute ma vie x-macros!
Alors maintenant, je définir 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 bonus, je peux aussi avoir le pré-processeur de construire mon 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 les 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
Mon préféré d'utilisation, cependant, est quand il s'agit de la communication des gestionnaires d'
Je crée d'abord un comms de la table, contenant chacune le nom de la commande et le code:
#define COMMAND_TABLE \
ENTRY(RESERVED, reserved, 0x00) \
ENTRY(COMMAND1, command1, 0x01) \
ENTRY(COMMAND2, command2, 0x02) \
...
ENTRY(COMMANDX, commandX, 0x0X) \
J'ai à la fois des majuscules et des minuscules dans les noms de la table, car le haut de cas seront utilisées pour les énumérations et les minuscules pour les noms de fonction.
Puis, j'ai aussi définir les structures pour chaque commande afin de définir ce que chaque commande ressemble à ceci:
typedef struct {...}command1_cmd_t;
typedef struct {...}command2_cmd_t;
etc.
De même, je définir des structures pour chaque réponse à la commande:
typedef struct {...}command1_resp_t;
typedef struct {...}command2_resp_t;
etc.
Ensuite, je peux définir mon code de commande de l'énumération:
enum
{
#define ENTRY(a,b,c) a##_CMD = c,
COMMAND_TABLE
#undef ENTRY
};
Je peux définir mon longueur de la commande de l'énumération:
enum
{
#define ENTRY(a,b,c) a##_CMD_LENGTH = sizeof(b##_cmd_t);
COMMAND_TABLE
#undef ENTRY
};
Je peux définir mon longueur de la réponse de l'énumération:
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 qui s'y sont 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)
NOTE: je n'ai jamais fait d'instancier la offset_struct_t, je viens de l'utiliser comme un moyen pour le compilateur de générer pour moi mon nombre de commandes de définition.
Remarque ensuite je peux générer mon tableau 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
Maintenant, enfin pour le plus cool jamais utiliser, je peux avoir le compilateur de calculer la taille de mon mémoire tampon de transmission devrait être.
/* 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 mon décalage struct, il n'est pas instancié, je peux utiliser l'opérateur sizeof à déclarer mes transmettre la taille de la mémoire tampon.
uint8_t tx_buf[sizeof(tx_buf_t)];
Maintenant, ma mémoire tampon de transmission tx_buf est la taille optimale et comme je l'ai ajouter des commandes à cette comms gestionnaire, mon tampon sera toujours la taille optimale. Cool!
Une autre utilisation est de créer de décalage tables: Vu que la mémoire est souvent une contrainte sur les systèmes embarqués, je ne veux pas utiliser de 512 octets pour mon saut de la table (2 octets par pointeur X 256 commandes possibles) quand c'est un tableau fragmenté. Au lieu de cela, je vais avoir une table de 8bit compensations pour chaque commande. Ce décalage est ensuite utilisé pour les index dans mon saut de la table qui désormais ne doit être NUM_COMMANDS * sizeof(pointeur). Dans mon cas, avec 10 commandes définies. Mon saut de la table est 20bytes longue et j'ai une table de décalage qui est de 256 octets, ce qui correspond à un total de 276bytes au lieu de 512bytes. J'ai ensuite appeler mes fonctions comme:
jump_table[offset_table[command]]();
au lieu de
jump_table[command]();
Je peux créer une table de décalage comme suit:
/* 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 bibliothèque standard macro définie dans "stddef.h"
D'un autre côté, il y a un moyen très facile de déterminer si un code de commande est pris en charge ou pas:
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 mon COMMAND_TABLE j'ai réservé octet de commande 0. Je peux créer une fonction appelée "process_reserved()" qui sera appelée si toute commande non valide octet est utilisé pour les index dans ma table de décalage.
X-les Macros sont essentiellement des modèles paramétrés. Donc ils sont dans le bon outil pour le travail si vous avez besoin de plusieurs choses semblables dans plusieurs formes. Ils vous permettent de créer une forme abstraite et de l'instancier selon des règles différentes.
J'utilise X-macros de sortie les valeurs de l'enum comme des chaînes de caractères. Et depuis le vivre, je préfère largement cette version, qui prend un "utilisateur" macro à appliquer à chaque élément. Plusieurs d'inclusion de fichier 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 suis aussi de les utiliser pour la fonction de répartition selon le type d'objet. De nouveau par le détournement de la même macro j'ai utilisé pour créer des énumérations.
#define TYPES(_) \
_(invalid) \
_(null) \
_(mark) \
_(integer) \
_(real) \
_(array) \
_(dict) \
_(save) \
_(name) \
_(string) \
/*enddef TYPES */
#define AS_TYPE(_) _ ## type ,
enum { TYPES(AS_TYPE) };
À l'aide de la macro garantit que tous mes indices de tableau correspondent à " l'associées les valeurs de l'enum, parce qu'ils construisent leurs diverses formes à l'aide de la nue-jetons à partir de la définition de la macro (les TYPES de macro).
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'aide de X-macros de cette façon vraiment aider le compilateur à donner des messages d'erreur utiles. J'ai omis le evalarray fonction de ce qui précède, car il permettrait de détourner l'attention de mon point de vue. Mais si vous tentez de compiler le code ci-dessus (les commentaires et les autres appels de fonction, et de fournir un mannequin typedef pour le contexte, bien sûr), le compilateur pourrait se plaindre d'un manque de fonction. Pour chaque nouveau type-je ajouter, je suis rappelé à ajouter un gestionnaire quand je recompile ce module. Ainsi, le X-macro sert à garantir que les structures parallèles restent intactes même que le projet se développe.
Edit:
Cette réponse a soulevé ma réputation 50%. Voici donc un petit plus. Ce qui suit est un exemple négatif, en réponse à la question quand ne pas utiliser X-Macros?
Cet exemple montre l'emballage de code arbitraire des fragments dans le X-"record". J'ai finalement abandonné cette branche du projet et de ne pas utiliser cette stratégie dans les projets ultérieurs (et pas faute d'avoir essayé). Il est devenu unweildy, en quelque sorte. En effet, la macro est nommé X6 parce qu'à un moment il y avait 6 arguments, mais je suis fatigué 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 de printf. Alors qu'il a l'air cool, c'est juste hocus pocus. Depuis, il est utilisé uniquement dans une fonction, d'une utilisation excessive de la macro est en fait séparée de l'information qui devrait être ensemble, et il rend la fonction illisible par lui-même. L'obfuscation est doublement malheureux dans une fonction de débogage comme celui-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
}
}
Donc, ne pas se laisser emporter. Comme je l'ai fait.