Ostatnio w pracy pojawił się problem - mamy pewną funkcjonalność opartą na identyfikację zasobu poprzez c-string, co wygląda mniej więcej tak:
Resource->Get("Something_Foo");
Przy czym filozofia była taka, aby nigdy nie zwracać błędy tylko coś w miarę sensownego (wartości domyślne). Takie podejście do identyfikacji ma zasadnicze wady:
- porównywanie stringów w run time
- łatwo o błędy - literówki, niezgodność wielkości liter
Zaleto-wadą jest też fakt, że nie trzeba definiować wcześniej tych napisów ale nie ma żadnej jednej, długiej listy.
Dlatego też w ramach refaktoryzacji postanowiliśmy zamienić napisy na typ wyliczeniowy (oraz użyć ich do szablonowych wytycznych, ale o tym napiszę w następnym poście).
W ten sposób nie będzie dość kosztownego porównywania napisów, a także możliwa jest weryfikacja poprawności nazwy w czasie kompilacji jak i wygoda dla programisty w postaci podpowiedzi.
enum res_id { Something_Foo, Something_Bar, Something_Foo_Foo };
const* char res_name[] = { "Something_Foo", "Something_Bar", "Something_Foo_Foo" };
Wypisanie tego w ten sposób i utrzymywanie w synchronizacji (biorąc pod uwagę tego, że osoby trzecie mogą zechcieć coś dopisać) nie wchodzi oczywiście w grę.
I tutaj pojawia się technika, którą nazywam "Generic Macro" (może jest na to jakaś inna nazwa?).
Otóż tworzymy plik o przykładowej nazwie resources.def w którym wpisujemy nasze dane wewnątrz jakiegoś w miarę sensownie nazwanego makra:
GENERIC_RESOURCE(Something_Foo) GENERIC_RESOURCE(Something_Bar) GENERIC_RESOURCE(SOmething_Foo_Foo)następnie wystarczy stworzyć plik res_id.hpp w którym zdefiniujemy makro tak jak chcemy a potem dołączymy nasz plik .def :
#ifndef RES_ID_HPP__
#define RES_ID_HPP__
// this will be defined in cpp file
extern const char* res_name[];
// include resource.def by using macro to transform it to enum type
enum res_id {
#define GENERIC_RESOURCE(X) X,
#include "resources.def"
#undef GENERIC_RESOURCE
MaxResId
};
#endif //RES_ID_HPP__
I teraz w pliku res_names.cpp:
#include "res_id.hpp"
const char* res_names[] = {
#define GENERIC_RESOURCE(X) (#X),
#include "resources.def"
#undef GENERIC_RESOURCE
"\0"
};
I w ten sposób wystarczy modyfikować tylko plik resources.def a zarówno lista napisów jak i typ wyliczeniowy zostaną wygenerowane i będą zsynchronizowane.
Jak widać czasem sprytnie użyte makra mogą przełożyć się na lepszą jakość kodu dzięki zmniejszeniu ryzyka popełnienia błędu :)
Może znajdziesz jakieś inne ciekawe zastosowanie tej techniki?