Uprzedzając krytykę: słyszałem o czymś takim jak debugger. Nie zawsze ma on jednak zastosowanie:
- debuggery często nie radzą sobie z bardziej zaawansowanymi typami zmiennych (zaczynając już od C++-owego std::string, o klasach w D nie wspominając),
- czasem zachodzi potrzeba sprawdzenia większej liczby przebiegów programu lub funkcji. Przechodzenie tego wszystkiego pod debuggerem to nieporozumienie,
- może nie być w ogóle możliwości użycia debuggera – n.p. w przypadku sprzedaży oprogramowania,
- wyciąganie armaty (debuggera) na mały problem-muchę również może być znaczącą przesadą.
Z powyższych powodów (ale głównie ostatniego), utworzyłem sobie proste makro w C++ pozwalające czytelnie przedstawiać zawartość zmiennych:
#define DBG(x) { cout << left << setw(Width) << #x << boolalpha << (x) << endl; } |
Dzięki zastosowaniu makra, mogę użyć operatora #, który zamienia podane mu tokeny preprocesora na odpowiadające im literały ciągów znakowych. Na przykładzie:
string foo = "bar"; int baz = 42; DBG(foo); DBG(foo+foo); DBG(baz); DBG(pow(baz, 2)); |
Spowoduje wypisanie:
foo bar foo+foo barbar baz 42 pow(baz, 2) 1764 |
Pracując nad Pierunem potrzebowałem czegoś podobnego w D. D jako taki nie obsługuje makr, jako powód podając głównie względy bezpieczeństwa oraz istnienie lepszych alternatyw. Ciężko się z takimi argumentami nie zgodzić – preprocesor w C i C++ to bardzo niewiele więcej niż find/replace na tekście, bez żadnego pojęcia o składni języka, przestrzeniach nazw, zakresach zmiennych itd. Niemniej jednak, w tym przypadku oznacza to, że nie można tak przyjemnie jak w C lub C++ zamienić tokenu na jego nazwę.
Istnieją za to wyżej wspomniane alternatywy. D, wyprzedzając o lata C++ (i inne języki), pozwala na bardzo zaawansowaną interpretację kodu w czasie kompilacji oraz na kompilację stringów jako kodu za pomocą mechanizmu o nazwie mixins (niekoniecznie to samo co to słowo znaczy w Ruby). Łącząc te dwie cechy można generować w czasie kompilacji kod, który zostanie skompilowany. W bardziej ekstremalnych przypadkach może to prowadzić do kompilacji kodu napisanego w innych językach do natywnego D (można powiedzieć, że tak się dzieje w ctRegex oraz w szablonach stron w vibe.d).
W prostszym przypadku, można zamienić string na zmienną/wyrażenie zawarte w nim – odwrotnie niż używając operatora # z C i C++, ale osiągając ten sam efekt:
template dbg(string S, string file = __FILE__, size_t line = __LINE__) { immutable string dbg = `{ import std.stdio, std.string; ` ~ `writefln("%s:%s: %-40s: %s",` ~ file.stringof ~ `, ` ~ line.stringof ~ `, "` ~ S ~ `", ` ~ S ~ `); }`; } |
Wykorzystanie, mixin(dbg!`kod tutaj`) jest niestety dużo brzydsze niż DBG(kod tutaj), ale istotny jest efekt końcowy, który i tak jest lepszy od ręcznego wpisywania danych do logów:
class foo{} struct x { int y; foo fooInstance; } void main() { import std.uni, std.math; int a = 42; x z = {1}; string s = "foo"; mixin(dbg!`z.y`); mixin(dbg!`z.fooInstance !is null`); mixin(dbg!`a.pow(2)`); } |
Wywołanie programu:
prog.d:22: z.y : 1 prog.d:23: z.fooInstance !is null : false prog.d:24: a.pow(2) : 1764 |