Реальное использование X-макросов


Я только что узнал X-Макросы. Какое реальное использование X-макросов вы видели? Когда они являются правильным инструментом для работы?

6 60

6 ответов:

я обнаружил X-макросы пару лет назад, когда начал использовать указатели функций в своем коде. Я встроенный программист и часто использую государственные машины. Часто я бы написал такой код:

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

проблема заключалась в том, что я считал, что очень подвержен ошибкам, чтобы поддерживать порядок моей таблицы указателей функций таким образом, чтобы он соответствовал порядку моего перечисления состояний.

мой друг познакомил меня с X-macros, и это было как лампочка отправился в моей голове. Серьезно, где ты был всю свою жизнь х-макрос!

Итак, теперь я определяю следующую таблицу:

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

и я могу использовать его следующим образом:

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

и

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

в качестве бонуса, я также могу иметь предварительный процессор построить мои прототипы функций следующим образом:

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

другое использование заключается в объявлении и инициализации регистров

#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

однако мое любимое использование когда дело доходит до обработчиков сообщений

сначала я создаю таблицу comms, содержащую имя и код каждой команды:

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

у меня есть как прописные, так и строчные имена в таблице, потому что верхний регистр будет использоваться для перечислений, а нижний-для имен функций.

затем я также определяю структуры для каждой команды, чтобы определить, как выглядит каждая команда:

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

etc.

аналогично я определяю структуры для каждой команды ответ:

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

etc.

тогда я могу определить мое перечисление кода команды:

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

я могу определить мое перечисление длины команды:

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

я могу определить мое перечисление длины ответа:

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

я могу определить, сколько команд есть следующим образом:

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)

примечание: Я никогда не создаю экземпляр offset_struct_t, я просто использую его как способ для компилятора генерировать для меня мое количество команд определение.

Примечание тогда я могу создать свою таблицу указателей функций следующим образом:

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

и мои прототипы функций:

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

теперь, наконец, для самого крутого использования когда-либо, я могу заставить компилятор вычислить, насколько большим должен быть мой буфер передачи.

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

снова это объединение похоже на мою структуру смещения, оно не создается, вместо этого я могу использовать оператор sizeof для объявления моего буфера передачи размер.

uint8_t tx_buf[sizeof(tx_buf_t)];

теперь мой буфер передачи tx_buf является оптимальным размером, и поскольку я добавляю команды к этому обработчику связи, мой буфер всегда будет оптимальным размером. Круто!

еще одно использование-это создание таблиц смещений: Поскольку память часто является ограничением для встроенных систем, я не хочу использовать 512 байт для моей таблицы переходов (2 байта на указатель X 256 возможных команд), когда это разреженный массив. Вместо этого у меня будет таблица 8-битных смещений для каждой возможной команды. Этот затем offset используется для индексирования в мою фактическую таблицу переходов, которая теперь должна быть только NUM_COMMANDS * sizeof(указатель). В моем случае с 10 команд определены. Моя таблица переходов имеет длину 20bytes, и у меня есть таблица смещения длиной 256 байт, которая в общей сложности составляет 276 байт вместо 512 байт. Затем я называю свои функции так:

jump_table[offset_table[command]]();

вместо

jump_table[command]();

я могу создать таблицу смещения следующим образом:

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

где offsetof-это стандартная библиотека макрос, определенный в "stddef.h"

в качестве побочного преимущества, есть очень простой способ определить, если код команды поддерживается или нет:

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

именно поэтому в моей COMMAND_TABLE я зарезервировал командный байт 0. Я могу создать одну функцию с именем "process_reserved ()", которая будет вызвана, если какой-либо недопустимый командный байт используется для индексирования в моей таблице смещения.

X-макросы по существу являются параметризованными шаблонами. Таким образом, они являются правильным инструментом для работы, если вам нужно несколько подобных вещей в нескольких обличьях. Они позволяют создать абстрактную форму и создать ее экземпляр в соответствии с различными правилами.

Я использую X-макросы для вывода значений перечисления в виде строк. И поскольку я столкнулся с этим, я сильно предпочитаю эту форму, которая принимает макрос "пользователя" для применения к каждому элементу. Включение нескольких файлов просто гораздо более болезненно для работы с.

/* 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]); */

Я также использую их для отправки функций на основе типа объекта. Опять же, захватив тот же макрос, который я использовал для создания значений перечисления.

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

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

использование макроса гарантирует, что все мои индексы массива будут соответствовать связанным значениям enum, потому что они строят свои различные формы, используя голые маркеры из определения макроса (макрос типов).

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);
}

использование X-макросов таким образом фактически помогает компилятору дать полезную ошибку сообщения. Я опустил функцию evalarray из вышеизложенного, потому что это отвлекло бы от моей точки зрения. Но если вы попытаетесь скомпилировать приведенный выше код (комментируя другие вызовы функций и предоставляя фиктивный typedef для контекста, конечно), компилятор будет жаловаться на отсутствующую функцию. Для каждого нового типа, который я добавляю, мне напоминают добавить обработчик, когда я перекомпилирую этот модуль. Таким образом, X-макрос помогает гарантировать, что параллельные структуры остаются нетронутыми даже в качестве проекта растет.

Edit:

этот ответ поднял мою репутацию на 50%. Так вот еще немного. Ниже приводится отрицательный пример, отвечая на вопрос: , когда не использовать X-макросы?

в этом примере показана упаковка фрагментов произвольного кода в X - "запись". В конце концов я отказался от этой ветви проекта и не использовал эту стратегию в более поздних проектах (и не из-за отсутствия попыток). Он стал каким-то невесомым. Действительно, макрос называется X6, потому что в какой-то момент было 6 аргументов, но я устал от изменения имени макроса.

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

одна большая проблема заключалась в строки форматирования функции printf. Хотя это выглядит круто, это просто фокус-покус. Поскольку он используется только в одной функции, чрезмерное использование макроса фактически отделяет информацию, которая должна быть вместе; и это делает функцию нечитаемой сама по себе. Обфускация вдвойне неудачна в функции отладки, такой как вот этот.

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

так что не увлекайтесь. Как и я.

в виртуальной машине Oracle HotSpot для языка программирования Java® есть файл globals.hpp, который использует RUNTIME_FLAGS В этак.

посмотреть исходный код:

мне нравится использовать макросы X для создания "богатых перечислений", которые поддерживают итерацию значений перечисления, а также получение строкового представления для каждого значения перечисления:

#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;
  }
};

Это не только определяет MouseButton::Value перечисление, это также позволяет мне делать такие вещи, как

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

я использую довольно массивный X-макрос для загрузки содержимого INI-файла в структуру конфигурации, среди прочего вращающуюся вокруг этой структуры.

это то, что моя "конфигурации.деф" -файл выглядит так:

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

это немного запутанно, я признаю. Быстро становится ясно, что я на самом деле не хочу писать все эти объявления типов после каждого поля-макроса. (Не волнуйтесь, есть большой комментарий, чтобы объяснить все, что я пропустил краткость.)

и вот как я объявляю структуру конфигурации:

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;

затем в коде сначала значения по умолчанию считываются в структуру конфигурации:

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

затем INI считывается в структуру конфигурации следующим образом, используя библиотеку 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

и переопределения из флагов командной строки, которые также форматируются с теми же именами (в GNU long form), применяются следующим образом в фойе с использованием библиотеки 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
    }
  }

и так далее, я также использую тот же макрос для печати вывода --help-flag и примера файла ini по умолчанию, конфигурации.def входит в мою программу 8 раз. "Квадратный колышек в круглое отверстие", может быть; как бы действительно компетентный программист поступил с этим? Много-много циклов и обработки строк?

https://github.com/whunmr/DataEx

используя следующие xmacros для создания класса c++, с serialize и deserialize functionlity builtin.

#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);

использование:

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_));
}

кроме того, другой пример находится в https://github.com/whunmr/msgrpc