Реальное использование X-Macros - PullRequest
65 голосов
/ 09 июля 2011

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

Ответы [ 6 ]

85 голосов
/ 22 февраля 2012

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

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

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

#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-битных смещений для каждой возможной команды.Это смещение затем используется для индексации в моей реальной таблице переходов, которая теперь должна быть только NUM_COMMANDS * sizeof (указатель).В моем случае с 10 определенными командами.Моя таблица переходов имеет длину 20 байт, и у меня есть таблица смещений длиной 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 ()", которая будет вызываться, если какой-либо недопустимый командный байт используется для индексации в моей таблице смещений.

33 голосов
/ 09 июля 2011

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

Я использую 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) };

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

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

Edit:

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

В этом примере показана упаковка произвольных фрагментов кода в 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
    }
}

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

6 голосов
/ 09 июля 2011

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

См. Исходный код:

4 голосов
/ 20 февраля 2015

Мне нравится использовать 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";
}
3 голосов
/ 17 июля 2015

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

Вот как выглядит мой файл "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. */

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

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

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), применяются следующим образом при использовании библиотеки 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 по умолчанию, файл configuration.def включен в мою программу 8 раз. «Квадратный колышек в круглое отверстие», может быть; как бы на самом деле поступил компетентный программист? Много-много циклов и обработка строк?

0 голосов
/ 09 марта 2017

https://github.com/whunmr/DataEx

Я использую следующий xmacros для генерации класса C ++ со встроенной функциональностью сериализации и десериализации.

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

...