X-Macro and DRY

Temat DRY był poruszany między innymi we wpisie o czystym kodzie. Tym razem zostanie zaprezentowana technika X-Macro oraz w jaki sposób ogranicza zbędne powtórzenia w kodzie.

W języku C występują makra – elementy języka podobne w działaniu do funkcji typu inline. W przeciwieństwie do funkcji, makra mogą być definiowane oraz usuwane. Podczas kompilacji w miejsce wywołania makr wstawiany jest wprost kod źródłowy. Operacja ta realizowana jest przez preprocesor.

Zdarza się sytuacja, gdzie trzeba zdefiniować encje z ich własnościami. Przykładowo mogłaby to być lista modeli samochodów. Takie encje mogłyby być zamodelowane w postaci zbioru struktur lub typu enumerowanego z funkcjami do pobierania tychże własności. W przypadku struktur powtarzany jest szablon struktury, natomiast w przypadku enumeracji powtarzana jest sekwencja identyfikatorów encji. Dodatkowo gdyby zaistniała potrzeba iterowania po wszystkich encjach, wówczas musi być utrzymywana dodatkowa lista elementów.

Technika X-Macro

W takiej sytuacji pomocna jest wspomniana w tytule technika X-Macro. Podstawą techniki jest zdefiniowana lista wywołać makra X (nazwa makra nie jest istotna). Następnie, w zależności od przypadku użycia takiej listy, makro X jest redefiniowane w sposób pożądany w danym kontekście. Może to być przykładowo makro definiujące typ enumerowany, makro generujące listę struktur bądź makro wypisujące nazwę każdej encji w postaci łańcucha znaków. Technikę prezentuje następujący fragment kodu:

/// list of properties
#define TYPE_LIST \
    ENUMDEF(BOOL) \
    ENUMDEF(CHAR) \
    ENUMDEF(INT) \
    ENUMDEF(DOUBLE) \
    ENUMDEF(FLOAT)

/// define enum type
#define ENUMDEF(item) item,
enum Types {
    TYPE_LIST
};
#undef ENUMDEF

/// define vector with enum items
#define ENUMDEF(item) item,
static std::vector<Types> types_list = {
    TYPE_LIST
};
#undef ENUMDEF

/// define structs
#define ENUMDEF(item) \
struct Type##item {  \
    const char *name = #item;     \
};
TYPE_LIST
#undef ENUMDEF

Powyższy kod zawiera definicję listy elementów TYPE_LIST (dla przykładu podstawowe typy języka) oraz generuje:

  • typ enumerowany Types z typami,
  • wektor types_list ze wszystkimi elementami typu Types,
  • typ struct dla każdego elementu listy TYPE_LIST.

Preprocesor powyższy kod rozwinie do następującej postaci:

enum Types {
    BOOL, CHAR, INT, DOUBLE, FLOAT,
};

static const std::vector<Types> enum_list = {
    BOOL, CHAR, INT, DOUBLE, FLOAT,
};

struct TypeBOOL { const char * name = "BOOL"; }; struct TypeCHAR { const char * name = "CHAR"; }; struct TypeINT { const char * name = "INT"; }; struct TypeDOUBLE { const char * name = "DOUBLE"; }; struct TypeFLOAT { const char * name = "FLOAT"; };

Lista TYPE_LIST może zawierać dodatkowe dane. W tym przypadku przykładowo mogłaby to być informacja czy dany typ jest stałoprzecinkowy oraz jego rozmiar wyrażony w bitach.

X-Macro and DRY

Wariant z include

W wariancie techniki X-Macro zamiast definiowania makra z listą (TYPE_LIST) elementy umieszcza się w dodatkowym nagłówku, który następnie jest include-owany w miejscu wywołań makra zawierającego listę (TYPE_LIST). Aby odróżnić taki plik od standardowego pliku nagłówkowego można przyjąć inne rozszerzenie. W projekcie kompilatora GCC przyjęto rozszerzenie .def. Wariant ten jest wygodniejszy w zastosowaniu, ponieważ nie trzeba stosować znaku kontynuacji definicji makra \ oraz stosowane są znaki końca linii zgodnie z tym jak są wprowadzone w pliku nagłówkowym.

Podsumowanie

Zaprezentowana technika jest ciekawym sposobem redukującym powtarzalność kodu. Mankamentem tej techniki może być to, że wygenerowany kod nie jest zapisany w pliku, w związku z tym niemożliwe jest wyszukanie wygenerowanego kodu poprzez przeszukiwanie drzewa kodu źródłowego.

Twój zespół potrzebuje wsparcia we wdrożeniu dobrych praktyk?


Leave a Reply

Your email address will not be published. Required fields are marked *