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 typuTypes
, - typ
struct
dla każdego elementu listyTYPE_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.

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