printf debugging – czytelne i wygodne wypisywanie zmiennych

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

link

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

link

Leave a Reply

Your email address will not be published.