Jeśli używasz kompilatora C99 lub nowszego
#define debug_print(fmt, ...) \
do { if (DEBUG) fprintf(stderr, fmt, __VA_ARGS__); } while (0)
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:
#ifdef DEBUG
#define DEBUG_TEST 1
#else
#define DEBUG_TEST 0
#endif
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ę:
#define debug_print(fmt, ...) \
do { if (DEBUG) fprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, \
__LINE__, __func__, __VA_ARGS__); } while (0)
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ę:
#define TRACE(x) do { if (DEBUG) dbg_printf x; } while (0)
A następnie w kodzie napisz:
TRACE(("message %d\n", var));
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:
#include <stdarg.h>
#include <stdio.h>
void dbg_printf(const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
vfprintf(stderr, fmt, args);
va_end(args);
}
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)
/*
@(#)File: $RCSfile: debug.h,v $
@(#)Version: $Revision: 1.2 $
@(#)Last changed: $Date: 1990/05/01 12:55:39 $
@(#)Purpose: Definitions for the debugging system
@(#)Author: J Leffler
*/
#ifndef DEBUG_H
#define DEBUG_H
/* -- Macro Definitions */
#ifdef DEBUG
#define TRACE(x) db_print x
#else
#define TRACE(x)
#endif /* DEBUG */
/* -- Declarations */
#ifdef DEBUG
extern int debug;
#endif
#endif /* DEBUG_H */
debug.h - wersja 3.6 (2008-02-11)
/*
@(#)File: $RCSfile: debug.h,v $
@(#)Version: $Revision: 3.6 $
@(#)Last changed: $Date: 2008/02/11 06:46:37 $
@(#)Purpose: Definitions for the debugging system
@(#)Author: J Leffler
@(#)Copyright: (C) JLSS 1990-93,1997-99,2003,2005,2008
@(#)Product: :PRODUCT:
*/
#ifndef DEBUG_H
#define DEBUG_H
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */
/*
** Usage: TRACE((level, fmt, ...))
** "level" is the debugging level which must be operational for the output
** to appear. "fmt" is a printf format string. "..." is whatever extra
** arguments fmt requires (possibly nothing).
** The non-debug macro means that the code is validated but never called.
** -- See chapter 8 of 'The Practice of Programming', by Kernighan and Pike.
*/
#ifdef DEBUG
#define TRACE(x) db_print x
#else
#define TRACE(x) do { if (0) db_print x; } while (0)
#endif /* DEBUG */
#ifndef lint
#ifdef DEBUG
/* This string can't be made extern - multiple definition in general */
static const char jlss_id_debug_enabled[] = "@(#)*** DEBUG ***";
#endif /* DEBUG */
#ifdef MAIN_PROGRAM
const char jlss_id_debug_h[] = "@(#)$Id: debug.h,v 3.6 2008/02/11 06:46:37 jleffler Exp $";
#endif /* MAIN_PROGRAM */
#endif /* lint */
#include <stdio.h>
extern int db_getdebug(void);
extern int db_newindent(void);
extern int db_oldindent(void);
extern int db_setdebug(int level);
extern int db_setindent(int i);
extern void db_print(int level, const char *fmt,...);
extern void db_setfilename(const char *fn);
extern void db_setfileptr(FILE *fp);
extern FILE *db_getfileptr(void);
/* Semi-private function */
extern const char *db_indent(void);
/**************************************\
** MULTIPLE DEBUGGING SUBSYSTEMS CODE **
\**************************************/
/*
** Usage: MDTRACE((subsys, level, fmt, ...))
** "subsys" is the debugging system to which this statement belongs.
** The significance of the subsystems is determined by the programmer,
** except that the functions such as db_print refer to subsystem 0.
** "level" is the debugging level which must be operational for the
** output to appear. "fmt" is a printf format string. "..." is
** whatever extra arguments fmt requires (possibly nothing).
** The non-debug macro means that the code is validated but never called.
*/
#ifdef DEBUG
#define MDTRACE(x) db_mdprint x
#else
#define MDTRACE(x) do { if (0) db_mdprint x; } while (0)
#endif /* DEBUG */
extern int db_mdgetdebug(int subsys);
extern int db_mdparsearg(char *arg);
extern int db_mdsetdebug(int subsys, int level);
extern void db_mdprint(int subsys, int level, const char *fmt,...);
extern void db_mdsubsysnames(char const * const *names);
#endif /* DEBUG_H */
Wariant z jednym argumentem dla C99 lub nowszy
Kyle Brandt zapytał:
W każdym razie, aby to zrobić, więc debug_print
nadal działa, nawet jeśli nie ma żadnych argumentów? Na przykład:
debug_print("Foo");
Jest jeden prosty, staromodny hack:
debug_print("%s\n", "Foo");
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:
#define debug_print(...) \
do { if (DEBUG) fprintf(stderr, __VA_ARGS__); } while (0)
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:
#define debug_print(fmt, ...) \
do { if (DEBUG) fprintf(stderr, fmt, ##__VA_ARGS__); } while (0)
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”?
Jaki jest cel tego 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 otoczenia do { ... } while (0)
, będziesz mieć:
/* BAD - BAD - BAD */
#define debug_print(...) \
if (DEBUG) fprintf(stderr, __VA_ARGS__)
Załóżmy teraz, że piszesz:
if (x > y)
debug_print("x (%d) > y (%d)\n", x, y);
else
do_something_useful(x, y);
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):
if (x > y)
{
if (DEBUG)
fprintf(stderr, "x (%d) > y (%d)\n", x, y);
else
do_something_useful(x, y);
}
Następna próba makra może być:
/* BAD - BAD - BAD */
#define debug_print(...) \
if (DEBUG) { fprintf(stderr, __VA_ARGS__); }
I ten sam fragment kodu wytwarza teraz:
if (x > y)
if (DEBUG)
{
fprintf(stderr, "x (%d) > y (%d)\n", x, y);
}
; // Null statement from semi-colon after macro
else
do_something_useful(x, y);
I to else
jest teraz błąd składniowy. W do { ... } while(0)
Unika pętla oba te problemy.
Istnieje inny sposób pisania makra, który może działać:
/* BAD - BAD - BAD */
#define debug_print(...) \
((void)((DEBUG) ? fprintf(stderr, __VA_ARGS__) : 0))
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 gdzie do { ... } 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ć tylko do { ... } 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
a mddebug.c
w
libsoq / src
podkatalogu.