piątek, 24 sierpnia 2012

Technika "Generic Macro"

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.

Ale tutaj jest pewien problem - potrzebna jest dalej reprezentacja tekstowa wartości typu wyliczeniowego - zarówno do zapisywania w logu ("Foo" mówi więcej niż 12) jak i API z którego korzystaliśmy również potrzebowało napisu... czyli chcemy mieć coś takiego:
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?

Brak komentarzy:

Prześlij komentarz