Próba utworzenia makra, którego można użyć do drukowania komunikatów debugowania po zdefiniowaniu DEBUG, jak na przykład następujący pseudo kod:
#define DEBUG 1
#define debug_print(args ...) if (DEBUG) fprintf(stderr, args)
Jak można to osiągnąć za pomocą makra?
c
c-preprocessor
jfarrell
źródło
źródło
Odpowiedzi:
Jeśli używasz kompilatora C99 lub nowszego
Zakłada się, że używasz C99 (notacja listy zmiennych zmiennych nie jest obsługiwana we wcześniejszych wersjach). Ten
do { ... } while (0)
idiom zapewnia, że kod działa jak instrukcja (wywołanie funkcji). Bezwarunkowe użycie kodu zapewnia, że kompilator zawsze sprawdza, czy kod debugowania jest prawidłowy - ale optymalizator usunie kod, gdy DEBUG wynosi 0.Jeśli chcesz pracować z #ifdef DEBUG, zmień warunek testu:
A następnie użyj DEBUG_TEST tam, gdzie użyłem DEBUG.
Jeśli upierasz się ciągiem znaków w ciągu formatu (prawdopodobnie dobrym pomysłem), można również wprowadzać takie rzeczy
__FILE__
,__LINE__
i__func__
do wyjścia, który może poprawić diagnostykę:Opiera się na konkatenacji ciągów, aby utworzyć ciąg w większym formacie niż pisze programista.
Jeśli używasz kompilatora C89
Jeśli utkniesz w C89 i nie ma przydatnego rozszerzenia kompilatora, nie ma szczególnie czystego sposobu na jego obsługę. Zastosowałem technikę:
A następnie w kodzie napisz:
Podwójne nawiasy są kluczowe - i dlatego masz zabawną notację w rozwinięciu makra. Tak jak poprzednio, kompilator zawsze sprawdza kod pod kątem poprawności składniowej (co jest dobre), ale optymalizator wywołuje funkcję drukowania tylko wtedy, gdy makro DEBUG ma wartość niezerową.
Wymaga to funkcji pomocniczej - w przykładzie dbg_printf () - do obsługi rzeczy takich jak „stderr”. Wymaga umiejętności pisania funkcji varargs, ale nie jest to trudne:
Możesz także użyć tej techniki w C99, oczywiście, ale
__VA_ARGS__
technika ta jest fajniejsza, ponieważ wykorzystuje zwykłą notację funkcji, a nie hack z podwójnymi nawiasami.Dlaczego kompilator zawsze widzi kod debugowania?
[ Przeróbki komentarzy do innej odpowiedzi. ]
Jedną z głównych idei obu powyższych implementacji C99 i C89 jest to, że właściwy kompilator zawsze widzi debugujące instrukcje printf. Jest to ważne w przypadku kodu długoterminowego - kodu, który będzie trwał dekadę lub dwie.
Załóżmy, że fragment kodu był w większości uśpiony (stabilny) przez wiele lat, ale teraz należy go zmienić. Ponownie włączasz śledzenie debugowania - ale debugowanie (śledzenie) kodu jest frustrujące, ponieważ odnosi się do zmiennych, które zostały zmienione lub zmienione, podczas lat stabilnej konserwacji. Jeśli kompilator (postprocesor wstępny) zawsze widzi instrukcję drukowania, zapewnia, że wszelkie otaczające zmiany nie unieważnią diagnostyki. Jeśli kompilator nie widzi instrukcji drukowania, nie może zabezpieczyć cię przed własną nieostrożnością (lub nieostrożnością współpracowników). Zobacz „ Praktykę programowania ” Kernighana i Pike'a, zwłaszcza rozdział 8 (patrz także Wikipedia na temat TPOP ).
Jest to doświadczenie „byłem tam, zrobiłem to” - użyłem zasadniczo techniki opisanej w innych odpowiedziach, w których wersja bez debugowania nie widzi instrukcji podobnych do printf przez wiele lat (ponad dekadę). Ale natknąłem się na poradę w TPOP (patrz mój poprzedni komentarz), a następnie włączyłem kod debugowania po kilku latach i napotkałem problemy ze zmienionym kontekstem przerywającym debugowanie. Kilka razy zawsze sprawdzanie poprawności drukowania uratowało mnie od późniejszych problemów.
Używam NDEBUGA tylko do kontrolowania asercji i osobnego makra (zwykle DEBUGA) do kontrolowania, czy śledzenie debugowania jest wbudowane w program. Nawet gdy wbudowane jest śledzenie debugowania, często nie chcę, aby wyniki debugowania były wyświetlane bezwarunkowo, więc mam mechanizm kontrolujący, czy dane wyjściowe pojawiają się (poziomy debugowania, a zamiast
fprintf()
bezpośredniego wywoływania , wywołuję funkcję drukowania debugowania, która drukuje tylko warunkowo więc ta sama kompilacja kodu może drukować lub nie drukować w zależności od opcji programu). Mam również wersję kodu dla wielu programów dla wielu podsystemów, dzięki czemu mogę mieć różne sekcje programu generujące różne ilości śladu - pod kontrolą środowiska wykonawczego.Opowiadam się za tym, aby dla wszystkich kompilacji kompilator widział instrukcje diagnostyczne; Jednak kompilator nie wygeneruje kodu dla instrukcji śledzenia debugowania, chyba że debugowanie jest włączone. Zasadniczo oznacza to, że cały kod jest sprawdzany przez kompilator za każdym razem, gdy kompilujesz - czy to w celu wydania, czy debugowania. To coś dobrego!
debug.h - wersja 1.2 (1990-05-01)
debug.h - wersja 3.6 (2008-02-11)
Wariant z jednym argumentem dla C99 lub nowszy
Kyle Brandt zapytał:
Jest jeden prosty, staromodny hack:
Przedstawione poniżej rozwiązanie tylko dla GCC również zapewnia wsparcie.
Możesz to jednak zrobić za pomocą prostego systemu C99, używając:
W porównaniu z pierwszą wersją tracisz ograniczone sprawdzanie, które wymaga argumentu „fmt”, co oznacza, że ktoś może spróbować wywołać „debug_print ()” bez argumentów (ale przecinek końcowy na liście argumentów
fprintf()
nie skompiluje się) . To, czy utrata kontroli w ogóle stanowi problem, jest dyskusyjne.Technika specyficzna dla GCC dla pojedynczego argumentu
Niektóre kompilatory mogą oferować rozszerzenia dla innych sposobów obsługi list argumentów o zmiennej długości w makrach. W szczególności, jak po raz pierwszy zauważono w komentarzach Hugo Ideler , GCC pozwala pominąć przecinek, który normalnie pojawiałby się po ostatnim „poprawionym” argumencie do makra. Pozwala również na użycie
##__VA_ARGS__
w tekście zastępowania makra, który usuwa przecinek poprzedzający notację, jeśli, ale tylko wtedy, gdy poprzedni token jest przecinkiem:To rozwiązanie zachowuje tę zaletę, że wymaga argumentu formatującego, jednocześnie akceptując opcjonalne argumenty po formacie.
Ta technika jest również obsługiwana przez Clang dla kompatybilności GCC.
Dlaczego pętla „do-while”?
Chcesz móc korzystać z makra, aby wyglądało jak wywołanie funkcji, co oznacza, że po nim nastąpi średnik. Dlatego musisz spakować ciało makra, aby było odpowiednie. Jeśli użyjesz
if
instrukcji bez otoczeniado { ... } while (0)
, będziesz mieć:Załóżmy teraz, że piszesz:
Niestety wcięcie to nie odzwierciedla faktycznej kontroli przepływu, ponieważ preprocesor tworzy kod równoważny temu (wcięcia i nawiasy klamrowe dodane w celu podkreślenia faktycznego znaczenia):
Następna próba makra może być:
I ten sam fragment kodu wytwarza teraz:
I to
else
jest teraz błąd składniowy. Wdo { ... } while(0)
Unika pętla oba te problemy.Istnieje inny sposób pisania makra, który może działać:
Pozostawia to fragment programu pokazany jako ważny. W
(void)
zapobiega lane to jest stosowane w sytuacjach, w których wymagana jest wartość - ale to może być używane jako lewego operandu operatora przecinkami gdziedo { ... } while (0)
wersja nie może. Jeśli uważasz, że powinieneś być w stanie osadzić kod debugowania w takich wyrażeniach, możesz to preferować. Jeśli wolisz, aby druk debugujący działał jako pełna instrukcja,do { ... } while (0)
wersja jest lepsza. Zauważ, że jeśli treść makra zawierała jakieś średniki (z grubsza mówiąc), możesz użyć tylkodo { ... } while(0)
zapisu. To zawsze działa; mechanizm wyrażenia wyrażenia może być trudniejszy do zastosowania. Możesz również otrzymać ostrzeżenia z kompilatora z formą wyrażenia, której wolisz unikać; będzie to zależeć od kompilatora i używanych flag.TPOP był wcześniej na http://plan9.bell-labs.com/cm/cs/tpop i http://cm.bell-labs.com/cm/cs/tpop, ale oba są teraz (2015-08-10) złamany.
Kod w GitHub
Jeśli jesteś ciekawy, można spojrzeć na ten kod w GitHub w moim SOQ (przepełnienie stosu pytań) repozytorium jako pliki
debug.c
,debug.h
amddebug.c
w libsoq / src podkatalogu.źródło
#define debug(...) \ do { if (DEBUG) \ printk("DRIVER_NAME:"); \ printk(__VA_ARGS__); \ printk("\n"); \ } while (0)
__FILE__, __LINE__, __func__, __VA_ARGS__
nie skompiluje się, jeśli nie masz parametrów printf, tj. Jeśli po prostu zadzwoniszdebug_print("Some msg\n");
Możesz to naprawić za pomocąfprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, ##__VA_ARGS__);
## __ VA_ARGS__ pozwala na przekazanie żadnych parametrów do funkcji.#define debug_print(fmt, ...)
i#define debug_print(...)
. Pierwszy z nich wymaga co najmniej jednego argumentu, ciągu formatującego (fmt
) i zero lub więcej innych argumentów; drugi wymaga łącznie zero lub więcej argumentów. Jeśli użyjeszdebug_print()
pierwszego, otrzymujesz od preprocesora błąd związany z niewłaściwym użyciem makra, podczas gdy drugi nie. Nadal jednak pojawiają się błędy kompilacji, ponieważ tekst zastępczy jest niepoprawny C. Zatem tak naprawdę nie ma dużej różnicy - stąd użycie terminu „ograniczone sprawdzanie”.-D input=4,macros=9,rules=2
do ustawienia poziomu debugowania systemu wejściowego na 4, a systemu makr na 9 (poddanego intensywnej kontroli ) i system reguł do 2. Istnieją niekończące się wariacje na temat; używaj tego, co ci odpowiada.Używam czegoś takiego:
Następnie używam D jako prefiksu:
Kompilator widzi kod debugowania, nie ma problemu z przecinkami i działa wszędzie. Działa również, gdy
printf
nie jest wystarczające, powiedzmy, kiedy musisz zrzucić tablicę lub obliczyć wartość diagnozy, która jest zbędna dla samego programu.EDYCJA: Ok, może generować problem, gdy jest
else
gdzieś w pobliżu, który może zostać przechwycony przez ten wstrzykniętyif
. To jest wersja, która go omija:źródło
for(;0;)
, może to generować problem, gdy piszesz coś takiego jakD continue;
lubD break;
.W przypadku implementacji przenośnej (ISO C90) można użyć podwójnych nawiasów, takich jak ten;
lub (hackish, nie polecam tego)
źródło
Oto wersja, której używam:
źródło
Zrobiłbym coś takiego
Myślę, że to jest czystsze.
źródło
assert()
ze stdlib działa w ten sam sposób i zwykle po prostu ponownie używamNDEBUG
makra do mojego własnego kodu debugowania ...Według http://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html powinno być
##
wcześniej__VA_ARGS__
.W przeciwnym razie, makro
#define dbg_print(format, ...) printf(format, __VA_ARGS__)
nie zostanie skompilowany następujący przykład:dbg_print("hello world");
.źródło
źródło
Oto, czego używam:
Zaletą jest prawidłowe obsługiwanie printf, nawet bez dodatkowych argumentów. W przypadku, gdy DBG == 0, nawet najgłupszy kompilator nie ma nic do żucia, więc nie jest generowany kod.
źródło
Moim ulubionym z poniższych jest
var_dump
, który nazywa się:var_dump("%d", count);
produkuje dane wyjściowe takie jak:
patch.c:150:main(): count = 0
Podziękowania dla @ „Jonathan Leffler”. Wszyscy są zadowoleni z C89:
Kod
źródło
Korzystając z gcc, lubię:
Ponieważ można go wstawić do kodu.
Załóżmy, że próbujesz debugować
Następnie możesz go zmienić na:
I możesz uzyskać analizę tego, jakie wyrażenie zostało ocenione na co.
Jest chroniony przed problemem podwójnej oceny, ale brak gensymów sprawia, że jest otwarty na kolizje nazw.
Jednak zagnieżdża się:
Myślę więc, że dopóki nie użyjesz g2rE3 jako nazwy zmiennej, wszystko będzie w porządku.
Z pewnością znalazłem to (i pokrewne wersje dla ciągów oraz wersje dla poziomów debugowania itp.) Bezcenne.
źródło
Dusiłem się, jak to zrobić od lat, i wreszcie znalazłem rozwiązanie. Nie wiedziałem jednak, że istnieją już tutaj inne rozwiązania. Po pierwsze, w odróżnieniu od odpowiedzi Lefflera , nie widzę jego argumentu, że odciski debugowania powinny zawsze być kompilowane. Wolę nie mieć ton niepotrzebnego kodu wykonującego się w moim projekcie, gdy nie jest to potrzebne, w przypadkach, gdy muszę przetestować i mogą one nie zostać zoptymalizowane.
Niekompilowanie za każdym razem może wydawać się gorsze niż w rzeczywistości. Kończą się odbitki debugujące, które czasem się nie kompilują, ale nie jest tak trudno skompilować i przetestować je przed sfinalizowaniem projektu. W tym systemie, jeśli używasz trzech poziomów debugowania, po prostu ustaw go na trzecim poziomie komunikatu debugowania, napraw błędy kompilacji i sprawdź, czy nie ma innych, zanim sfinalizujesz swój kod. (Ponieważ oczywiście kompilowanie instrukcji debugowania nie gwarantuje, że nadal działają zgodnie z przeznaczeniem).
Moje rozwiązanie zapewnia również poziomy szczegółowości debugowania; a jeśli ustawisz go na najwyższym poziomie, wszystkie się kompilują. Jeśli ostatnio korzystałeś z wysokiego poziomu szczegółowości debugowania, wszystkie były w stanie skompilować w tym czasie. Ostateczne aktualizacje powinny być dość łatwe. Nigdy nie potrzebowałem więcej niż trzech poziomów, ale Jonathan mówi, że użył dziewięciu. Ta metoda (podobnie jak Leffler) może zostać rozszerzona na dowolną liczbę poziomów. Zastosowanie mojej metody może być prostsze; wymagając tylko dwóch instrukcji, gdy są używane w kodzie. Jednak koduję również makro CLOSE - chociaż to nic nie robi. Może gdybym wysłał do pliku.
W porównaniu z kosztami dodatkowy etap testowania ich pod kątem kompilacji przed dostawą jest taki
Gałęzie są w rzeczywistości stosunkowo dość kosztowne we współczesnych procesorach pobierania wstępnego. Może nie jest to wielka sprawa, jeśli twoja aplikacja nie jest krytyczna czasowo; ale jeśli wydajność stanowi problem, to tak, na tyle duża sprawa, że wolałbym wybrać nieco szybciej wykonywany kod debugowania (i być może szybsze wydanie, w rzadkich przypadkach, jak wspomniano).
Tak więc chciałem makro debugowania wydruku, które nie kompiluje się, jeśli nie ma być drukowane, ale robi, jeśli tak jest. Chciałem też poziomów debugowania, aby np. Gdybym chciał, aby kluczowe fragmenty kodu, które mają kluczowe znaczenie dla wydajności, nie były drukowane w niektórych momentach, ale drukowały w innych, mogłem ustawić poziom debugowania i uruchomić dodatkowe odbitki debugujące. natknął się na sposób implementacji poziomów debugowania, które określają, czy wydruk został nawet skompilowany, czy nie. Osiągnąłem to w ten sposób:
DebugLog.h:
DebugLog.cpp:
Korzystanie z makr
Aby go użyć, po prostu wykonaj:
Aby zapisać do pliku dziennika, po prostu wykonaj:
Aby go zamknąć, wykonujesz:
chociaż obecnie nie jest to nawet konieczne z technicznego punktu widzenia, ponieważ nic nie robi. Nadal jednak używam CLOSE, na wypadek, gdy zmienię zdanie na temat tego, jak to działa i chcę pozostawić otwarty plik między instrukcjami rejestrowania.
Następnie, jeśli chcesz włączyć drukowanie debugowania, po prostu edytuj pierwszy #define w pliku nagłówkowym, aby powiedzieć, np
Aby instrukcje logowania były kompilowane do zera, zrób
Jeśli potrzebujesz informacji z często wykonywanego fragmentu kodu (tj. Wysokiego poziomu szczegółowości), możesz napisać:
Jeśli zdefiniujesz DEBUG jako 3, kompilujesz poziomy rejestrowania 1, 2 i 3. Jeśli ustawisz na 2, otrzymasz poziomy rejestrowania 1 i 2. Jeśli ustawisz na 1, otrzymasz tylko instrukcje rejestrowania na poziomie 1.
Jeśli chodzi o pętlę „do-while”, ponieważ ocenia się na jedną funkcję lub na nic, zamiast instrukcji if, pętla nie jest potrzebna. OK, przekonaj mnie, że używam C zamiast C ++ IO (i QS's QString :: arg () to bezpieczniejszy sposób formatowania zmiennych również w Qt - jest dość sprytny, ale wymaga więcej kodu, a dokumentacja formatowania nie jest tak zorganizowana jak to może być - ale nadal znalazłem przypadki, w których jest to preferowane), ale możesz umieścić dowolny kod w pliku .cpp, który chcesz. Może to być także klasa, ale wtedy musisz utworzyć jej instancję i nadążyć za nią lub zrobić nową () i zapisać ją. W ten sposób po prostu upuszczasz instrukcje #include, init i opcjonalnie zamykasz je w swoim źródle i możesz zacząć z niego korzystać. Byłaby to jednak dobra klasa, gdybyś był tak skłonny.
Wcześniej widziałem wiele rozwiązań, ale żadne nie spełniało moich kryteriów, tak jak i tego.
Nie strasznie znaczące, ale dodatkowo:
DEBUGLOG_LOG(3, "got here!");
); dzięki czemu możesz używać np. bezpieczniejszego formatowania .arg () w Qt. Działa na MSVC, a zatem prawdopodobnie gcc. Używa##
w#define
s, co jest niestandardowe, jak wskazuje Leffler, ale jest szeroko obsługiwane. (Możesz przekodować go, aby nie używać,##
jeśli to konieczne, ale będziesz musiał użyć hacka, takiego jak on.)Ostrzeżenie: jeśli zapomnisz podać argument poziomu rejestrowania, MSVC bezskutecznie twierdzi, że identyfikator nie został zdefiniowany.
Możesz użyć nazwy symbolu preprocesora innej niż DEBUG, ponieważ niektóre źródła również definiują ten symbol (np. Postępuje przy użyciu
./configure
poleceń w celu przygotowania do budowy). Kiedy to opracowałem, wydawało mi się to naturalne. Opracowałem go w aplikacji, w której DLL jest używany przez coś innego, i bardziej zwyczajowo jest wysyłanie wydruków dziennika do pliku; ale zmiana na vprintf () również działałaby dobrze.Mam nadzieję, że to oszczędza wielu z was żalu z powodu znalezienia najlepszego sposobu rejestrowania debugowania; lub pokaże Ci ten, który wolisz. Przez całe dziesięciolecia bez przekonania próbowałem to rozgryźć. Działa w MSVC 2012 i 2015, a więc prawdopodobnie na gcc; jak również prawdopodobnie pracuje nad wieloma innymi, ale nie przetestowałem ich na nich.
Mam na myśli także stworzenie wersji tego streamingu jednego dnia.
Uwaga: Podziękowania dla Lefflera, który serdecznie pomógł mi lepiej sformatować moją wiadomość dla StackOverflow.
źródło
if (DEBUG)
instrukcji w czasie wykonywania, które nie są optymalizowane” - co przechyla się w wiatrakach . Cały sens opisanego przeze mnie systemu polega na tym, że kod jest sprawdzany przez kompilator (ważne i automatyczne - nie jest wymagana specjalna kompilacja), ale kod debugowania wcale nie jest generowany, ponieważ jest zoptymalizowany (więc nie ma wpływu na środowisko uruchomieniowe) rozmiar lub wydajność kodu, ponieważ kod nie jest obecny w czasie wykonywania).((void)0)
- to proste.Wierzę, że ta odmiana motywu daje kategorie debugowania bez potrzeby posiadania osobnej nazwy makra dla każdej kategorii.
Użyłem tej odmiany w projekcie Arduino, w którym przestrzeń programu jest ograniczona do 32 KB, a pamięć dynamiczna jest ograniczona do 2 KB. Dodanie instrukcji debugowania i śledzenia ciągów debugowania szybko zajmuje miejsce. Dlatego niezbędna jest możliwość ograniczenia śledzenia debugowania dołączanego podczas kompilacji do niezbędnego minimum za każdym razem, gdy budowany jest kod.
debug.h
wywoływanie pliku .cpp
źródło