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?

środa, 1 sierpnia 2012

(Bezsensowne) Przeładowania operatorów w C++

Przy pomocy przeładowania operatorów w C++ można robić bezsensowne rzeczy, które jednych zachwycą a u innych wywołają wrogość :) Jedną z takich rzeczy jest stworzenie obsługi list (np: zakupów, TODO, aliasów) czy też listy wad i zalet w taki "naturalny" sposób:
Aliases aliasList;
aliasList.add_for("number")
    * "one" 
    * "two" 
    * "three";

aliasList.add_for("moo")
    - "foo"
    - "bar"
    - "jaj";

aliasList.add_for("no_alias"); // identity

std::cout << "three stands for " << aliasList["three"] << '\n';
List list;

list.prons_and_cons_for("headphones X")
    + "price"
    + "quality"
    + "bass"
    - "too short cable"
    - "no volume control";

list.print_info_about("headphones X"); // or list.get_cons() etc
Implementacja polega na rozbiciu na dwie klasy.
Pierwsza, najważniejsza klasa polega na tym, że akumuluje argumenty (albo w moim przypadku - posiada referencje do kontenera drugiej klasy) w zadany sposób poprzez przeładowanie operatorów zwracających referencję - tak jak przeładowuje się zwykle operatory ">>" oraz "<<", przy czym trzeba również pamiętać o priorytetach i kierunku łączenia.
Druga klasa (Aliases, List w przykładach) zajmuje się tworzeniem pierwszej klasy i zawiera kontenery i właściwą logikę.

Jeśli nie jest to jeszcze jasne, to wystarczy spojrzeć na kod (drafty):