Rzeczywiste wykorzystanie makr X

Właśnie dowiedziałem się o x-makrach . Jakie rzeczywiste zastosowania X-makr widziałeś? Kiedy są one odpowiednie narzędzie do pracy?

Author: Brian Tompsett - 汤莱恩, 2011-07-09

6 answers

Odkryłem X-makra kilka lat temu, kiedy zacząłem używać wskaźników funkcji w moim kodzie. Jestem programistą wbudowanym i często korzystam z maszyn stanowych. Często pisałbym taki kod:

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

Problem polegał na tym, że uważałem, że jest to bardzo podatne na błędy, aby utrzymać kolejność tabeli wskaźników funkcji, tak aby odpowiadała kolejności moich wyliczeń Stanów.

Mój przyjaciel wprowadził mnie do X-makr i to było jak Żarówka wybuchło mi w głowie. Poważnie, gdzie byłeś całe moje życie x-makra!

Więc teraz definiuję następującą tabelę:

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

I mogę go używać w następujący sposób:

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

I

p_func_t jumptable[NUM_STATES] =
{
#define ENTRY(a,b) b,
    STATE_TABLE
#undef ENTRY
};
Jako bonus, mogę również mieć pre-procesor zbudować prototypy mojej funkcji w następujący sposób:]}
#define ENTRY(a,b) static void b(void);
    STATE_TABLE
#undef ENTRY

Innym zastosowaniem jest deklarowanie i inicjalizacja rejestrów

#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

Moim ulubionym zastosowaniem jest jednak, jeśli chodzi o obsługę komunikacji

Najpierw tworzę tabela komend, zawierająca nazwę i Kod każdego polecenia:

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

Mam zarówno wielkie jak i małe nazwy w tabeli, ponieważ wielkie litery będą używane dla wyliczeń, a małe dla nazw funkcji.

Następnie definiuję również struktury dla każdego polecenia, aby zdefiniować, jak każde polecenie wygląda:

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

etc.

Podobnie definiuję struktury dla każdej odpowiedzi polecenia:

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

etc.

Wtedy mogę zdefiniować wyliczenie kodu polecenia:

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

Mogę zdefiniować moje polecenie Długość:

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

Mogę określić długość odpowiedzi:

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

Mogę określić, ile komend jest w następujący sposób:

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)

Uwaga: nigdy nie tworzyłem instancji offset_struct_t, po prostu używam jej jako sposobu, aby kompilator wygenerował dla mnie definicję mojej liczby poleceń.

Następnie mogę wygenerować tabelę wskaźników funkcji w następujący sposób:

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

I moje prototypy funkcji:

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

Teraz wreszcie dla najfajniejsze użycie kiedykolwiek, mogę mieć kompilator obliczyć, jak duży powinien być mój bufor transmisji.

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

Ponownie ta Unia jest jak moja struktura offsetowa, nie jest instancyjna, zamiast tego mogę użyć operatora sizeof do zadeklarowania rozmiaru bufora transmisji.

uint8_t tx_buf[sizeof(tx_buf_t)];

Teraz mój bufor transmisji tx_buf jest optymalnym rozmiarem i kiedy dodaję polecenia do tego programu obsługi komunikatorów, mój bufor zawsze będzie optymalnym rozmiarem. Super!

Innym zastosowaniem jest tworzenie tabel offsetowych: Ponieważ pamięć jest często ograniczenie w systemach wbudowanych, Nie chcę używać 512 bajtów dla mojej tabeli skoku (2 bajty na wskaźnik x 256 możliwych poleceń), gdy jest to rzadka tablica. Zamiast tego będę miał tabelę 8-bitowych offsetów dla każdego możliwego polecenia. To przesunięcie jest następnie używane do indeksowania do mojej rzeczywistej tabeli skoków, która teraz musi być NUM_COMMANDS * sizeof (pointer). W moim przypadku z 10 poleceń zdefiniowanych. Moja tabela skoku ma długość 20bajtów i mam tabelę przesunięcia, która ma długość 256 bajtów, co w sumie wynosi 276bajtów zamiast 512bytes. Następnie wywołuję swoje funkcje w ten sposób:

jump_table[offset_table[command]]();

Zamiast

jump_table[command]();

Mogę utworzyć tabelę offsetową w następujący sposób:

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

Gdzie offsetof jest makrem biblioteki standardowej zdefiniowanym w " stddef.h "

Jako korzyść poboczna, istnieje bardzo łatwy sposób, aby określić, czy kod polecenia jest obsługiwany, czy nie:

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

Dlatego też w COMMAND_TABLE zarezerwowałem bajt polecenia 0. Mogę utworzyć jedną funkcję o nazwie "process_reserved ()", która będzie wywołane, jeśli jakikolwiek nieprawidłowy bajt polecenia jest używany do indeksowania do mojej tabeli offsetowej.

 77
Author: ACRL,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-02-26 18:35:16

X-makra są zasadniczo parametryzowanymi szablonami. Są więc odpowiednim narzędziem do pracy, jeśli potrzebujesz kilku podobnych rzeczy w kilku wersjach. Pozwalają one stworzyć abstrakcyjną formę i utworzyć ją według różnych zasad.

Używam x-makr do wypisywania wartości enum jako ciągów znaków. I od momentu napotkania go, zdecydowanie wolę tę formę, która przyjmuje makro" użytkownika", aby zastosować do każdego elementu. Włączenie wielu plików jest o wiele bardziej bolesne.

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

I ' m używa ich również do wysyłania funkcji na podstawie typu obiektu. Ponownie przejmując to samo makro, którego użyłem do stworzenia wartości enum.

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

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

Użycie makra gwarantuje, że wszystkie moje indeksy tablicy będą pasowały do powiązanych wartości enum, ponieważ konstruują swoje różne formy za pomocą gołych tokenów z definicji makra (typy makro).

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

Używanie makr X w ten sposób pomaga kompilatorowi w przekazywaniu przydatnych komunikatów o błędach. Pominąłem funkcję evalarray z powyższego bo to odwróci moją uwagę. Ale jeśli spróbujesz skompilować powyższy kod (komentując inne wywołania funkcji i dostarczając atrapy typedef dla kontekstu, oczywiście), kompilator będzie narzekał na brakującą funkcję. Dla każdego nowego typu, który dodaję, przypomina mi się, aby dodać obsługę podczas rekompilacji tego modułu. Tak więc X-macro pomaga zagwarantować, że równoległe struktury pozostają nienaruszone nawet w miarę rozwoju projektu.

Edit:

[[5]] ta odpowiedź wzbudziła moje reputacja 50%. To jeszcze trochę. Poniżej znajduje się negatywny przykład , odpowiadający na pytanie: Kiedy Nie używać makr X?

Ten przykład pokazuje pakowanie dowolnych fragmentów kodu do X - "rekordu". Ostatecznie porzuciłem tę gałąź projektu i nie wykorzystałem tej strategii w późniejszych projektach (i nie z chęci wypróbowania). Jakoś to się stało nieciekawe. Rzeczywiście makro nazywa się X6 bo w pewnym momencie było 6 argumentów, ale dostałem zmęczony zmianą nazwy makra.

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

Dużym problemem były ciągi formatu printf. Chociaż wygląda fajnie, to tylko Hokus Pokus. Ponieważ jest używany tylko w jednej funkcji, nadużywanie makra faktycznie oddzielone informacje, które powinny być razem; i to sprawia, że funkcja nieczytelna sama w sobie. Zaciemnienie jest podwójnie niefortunne w funkcji debugowania, takiej jak ta.

//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
    }
}
Więc nie daj się ponieść emocjom. Tak jak ja.
 32
Author: luser droog,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-07-26 09:20:51

W maszynie wirtualnej Oracle HotSpot dla języka programowania Java® znajduje się plik globals.hpp, który używa w ten sposób RUNTIME_FLAGS.

Zobacz kod źródłowy:

 5
Author: Roland Illig,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-08-27 09:00:27

Lubię używać makr X do tworzenia 'rich enumerations', które wspierają iterację wartości enum, a także uzyskiwanie reprezentacji łańcuchowej dla każdej wartości enum:

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

To nie tylko definiuje MouseButton::Value enum, ale także pozwala mi robić takie rzeczy jak

// Print names of all supported mouse buttons
for ( const int *mb = MouseButton::values(); *mb != -1; ++mb ) {
    std::cout << MouseButton::valueAsString( (MouseButton::Value)*mb ) << "\n";
}
 4
Author: Frerich Raabe,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-02-20 09:23:25

Używam dość masywnego x-makra, aby załadować zawartość pliku INI do struktury konfiguracyjnej, między innymi obracającej się wokół tej struktury.

Oto moja " konfiguracja.def " - plik wygląda następująco:

#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. */
Przyznaję, że to trochę zagmatwane. Szybko stało się jasne, że tak naprawdę nie chcę pisać tych wszystkich deklaracji typu po każdym polu-makrze. (Nie martw się, jest duży komentarz, aby wyjaśnić wszystko, co pominąłem dla zwięzłości.)

I tak deklaruję struktura konfiguracji:

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;

Następnie, w kodzie, najpierw wartości domyślne są odczytywane do struktury konfiguracyjnej:

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

Następnie, INI jest odczytywane do struktury konfiguracyjnej w następujący sposób, używając biblioteki 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

I przesłonięcia z FLAG linii poleceń, które również są sformatowane z tymi samymi nazwami (w długiej formie GNU), są stosowane w następujący sposób w followingu przy użyciu biblioteki 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
    }
  }

I tak dalej, używam również tego samego makra do drukowania -- help -znacznik wyjściowy i przykładowy domyślny plik ini, konfiguracja.def jest włączony 8 razy w moim programie. "Kwadratowy kołek w okrągły otwór", może; jak właściwie Kompetentny programista postąpiłby z tym? Dużo pętli i przetwarzania strun?

 3
Author: VITTUIX-MAN,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-07-17 15:56:28

Https://github.com/whunmr/DataEx

Używanie następujących xmacros do generowania klasy c++, z wbudowaną funkcjonalnością serialize i deserialize.

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

Użycie:

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

Również inny przykład jest w https://github.com/whunmr/msgrpc

 0
Author: whunmr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-03-09 09:32:52