# Zdefiniować makro do drukowania debugowania w C?

209

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?

jfarrell
źródło
Czy kompilator (gcc) zoptymalizuje instrukcje, np. Jeśli (DEBUG) {...} jest wyłączony, jeśli w kodzie produkcyjnym makro DEBUG jest ustawione na 0? Rozumiem, że istnieją dobre powody, aby pozostawić polecenia debugowania widoczne dla kompilatora, ale złe przeczucie pozostaje. -Pat
Pat

Odpowiedzi:

410

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_printnadal 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 ifinstrukcji 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 elsejest 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.ha mddebug.cw libsoq / src podkatalogu.

Jonathan Leffler
źródło
1
Myślę, że warto wspomnieć o podejściu GCC ## z gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html pod nagłówkiem „Wariant C99 z jednym argumentem”.
Hugo Ideler,
2
Wiele lat później ta odpowiedź jest nadal najbardziej użyteczna ze wszystkich stron internetowych, na temat aliasu printk! vfprintf nie działa w przestrzeni jądra, ponieważ stdio nie jest dostępne. Dziękuję Ci! #define debug(...) \ do { if (DEBUG) \ printk("DRIVER_NAME:"); \ printk(__VA_ARGS__); \ printk("\n"); \ } while (0)
kevinf
6
W twoim przykładzie ze słowami kluczowymi __FILE__, __LINE__, __func__, __VA_ARGS__nie skompiluje się, jeśli nie masz parametrów printf, tj. Jeśli po prostu zadzwonisz debug_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.
mc_electron
1
@LogicTom: różnica jest pomiędzy #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żyjesz debug_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”.
Jonathan Leffler
1
Wariant pokazany powyżej, @ St.Antario, wykorzystuje jeden aktywny poziom debugowania w całej aplikacji, i zwykle używam opcji wiersza poleceń, aby umożliwić ustawienie poziomu debugowania podczas uruchamiania programu. Mam również wariant, który rozpoznaje wiele różnych podsystemów, z których każdy otrzymuje nazwę i własny poziom debugowania, dzięki czemu mogę użyć -D input=4,macros=9,rules=2do 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.
Jonathan Leffler,
28

Używam czegoś takiego:

#ifdef DEBUG
 #define D if(1) 
#else
 #define D if(0) 
#endif

Następnie używam D jako prefiksu:

D printf("x=%0.3f\n",x);

Kompilator widzi kod debugowania, nie ma problemu z przecinkami i działa wszędzie. Działa również, gdy printfnie 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 elsegdzieś w pobliżu, który może zostać przechwycony przez ten wstrzyknięty if. To jest wersja, która go omija:

#ifdef DEBUG
 #define D 
#else
 #define D for(;0;)
#endif
mbq
źródło
3
Jeśli chodzi o for(;0;), może to generować problem, gdy piszesz coś takiego jak D continue;lub D break;.
ACcreator
1
Masz mnie; wydaje się jednak bardzo mało prawdopodobne, że może się zdarzyć przypadkowo.
mbq
11

W przypadku implementacji przenośnej (ISO C90) można użyć podwójnych nawiasów, takich jak ten;

#include <stdio.h>
#include <stdarg.h>

#ifndef NDEBUG
#  define debug_print(msg) stderr_printf msg
#else
#  define debug_print(msg) (void)0
#endif

void
stderr_printf(const char *fmt, ...)
{
  va_list ap;
  va_start(ap, fmt);
  vfprintf(stderr, fmt, ap);
  va_end(ap);
}

int
main(int argc, char *argv[])
{
  debug_print(("argv[0] is %s, argc is %d\n", argv[0], argc));
  return 0;
}

lub (hackish, nie polecam tego)

#include <stdio.h>

#define _ ,
#ifndef NDEBUG
#  define debug_print(msg) fprintf(stderr, msg)
#else
#  define debug_print(msg) (void)0
#endif

int
main(int argc, char *argv[])
{
  debug_print("argv[0] is %s, argc is %d"_ argv[0] _ argc);
  return 0;
}
Marcin Koziuk
źródło
3
@LB: aby preprocesor „pomyślał”, istnieje tylko jeden argument, pozwalając na rozwinięcie _ na późniejszym etapie.
Marcin Koziuk
10

Oto wersja, której używam:

#ifdef NDEBUG
#define Dprintf(FORMAT, ...) ((void)0)
#define Dputs(MSG) ((void)0)
#else
#define Dprintf(FORMAT, ...) \
    fprintf(stderr, "%s() in %s, line %i: " FORMAT "\n", \
        __func__, __FILE__, __LINE__, __VA_ARGS__)
#define Dputs(MSG) Dprintf("%s", MSG)
#endif
Christoph
źródło
9

Zrobiłbym coś takiego

#ifdef DEBUG
#define debug_print(fmt, ...) fprintf(stderr, fmt, __VA_ARGS__)
#else
#define debug_print(fmt, ...) do {} while (0)
#endif

Myślę, że to jest czystsze.

LB40
źródło
Nie podoba mi się pomysł użycia makra w teście jako flagi. Czy możesz wyjaśnić, dlaczego drukowanie debugowania powinno być zawsze sprawdzane?
LB40
1
@Jathanathan: Jeśli kod zostanie kiedykolwiek wykonany tylko w trybie debugowania, dlaczego miałbyś się przejmować, jeśli kompiluje się w trybie innym niż debugowanie? assert()ze stdlib działa w ten sam sposób i zwykle po prostu ponownie używam NDEBUGmakra do mojego własnego kodu debugowania ...
Christoph
używając DEBUGA w teście, jeśli ktoś zrobi niekontrolowany undef DEBUG, twój kod nie będzie się kompilował. dobrze ?
LB40,
4
Udostępnianie debugowania jest frustrujące, a następnie debugowanie kodu debugowania, ponieważ odnosi się do zmiennych, których nazwy zmieniono lub ponownie wprowadzono itp. Jeśli kompilator (postprocesor) zawsze widzi instrukcję print, 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ą kolegów lub współpracowników). Zobacz „Praktykę programowania” Kernighana i Pike'a - plan9.bell-labs.com/cm/cs/tpop .
Jonathan Leffler
1
@Christoph: cóż, w pewnym sensie ... Używam NDEBUGA tylko do kontroli asercji i osobnego makra (zwykle DEBUGA) do kontroli śledzenia debugowania. Często nie chcę, aby dane wyjściowe debugowania pojawiały się bezwarunkowo, więc mam mechanizm kontrolujący, czy dane wyjściowe pojawiają się (poziomy debugowania i zamiast bezpośrednio wywoływać funkcję fprintf (), wywołuję funkcję drukowania debugowania, która warunkowo drukuje tylko taką samą kompilację kod może drukować lub nie drukować w zależności od opcji programu). Opowiadam się za tym, aby dla wszystkich kompilacji kompilator widział instrukcje diagnostyczne; jednak nie wygeneruje kodu, dopóki debugowanie nie zostanie włączone.
Jonathan Leffler
8

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");.

Chobits Tai
źródło
1
Witamy w Stack Overflow. Masz rację, że GCC ma niestandardowe rozszerzenie, do którego się odwołujesz. Aktualnie akceptowana odpowiedź tak naprawdę wspomina o tym, w tym dokładnie podany przez Ciebie adres URL.
Jonathan Leffler
7
#define debug_print(FMT, ARGS...) do { \
    if (DEBUG) \
        fprintf(stderr, "%s:%d " FMT "\n", __FUNCTION__, __LINE__, ## ARGS); \
    } while (0)
eyalm
źródło
Która wersja C obsługuje tę notację? A jeśli zadziałało, token wklejający wszystkie takie argumenty oznacza, że ​​masz tylko bardzo ograniczony zestaw opcji dla ciągu formatu, prawda?
Jonathan Leffler
@Jonathan: gcc (Debian 4.3.3-13) 4.3.3
eyalm
1
OK - uzgodnione: jest udokumentowane jako stare rozszerzenie GNU (sekcja 5.17 podręcznika GCC 4.4.1). Ale prawdopodobnie powinieneś udokumentować, że będzie działać tylko z GCC - a może zrobiliśmy to między nami w tych komentarzach.
Jonathan Leffler,
1
Moim zamiarem było pokazanie innego stylu używania argumentów, a głównie zademonstrowanie użycia FUNKCJI i LINII
eyalm,
2

Oto, czego używam:

#if DBG
#include <stdio.h>
#define DBGPRINT printf
#else
#define DBGPRINT(...) /**/  
#endif

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.

5tenzel
źródło
Lepiej jest, aby kompilator zawsze sprawdzał kod debugowania.
Jonathan Leffler
1

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

#define DEBUG 1
#include <stdarg.h>
#include <stdio.h>
void debug_vprintf(const char *fmt, ...)
{
    va_list args;
    va_start(args, fmt);
    vfprintf(stderr, fmt, args);
    va_end(args);
}

/* Call as: (DOUBLE PARENTHESES ARE MANDATORY) */
/* var_debug(("outfd = %d, somefailed = %d\n", outfd, somefailed)); */
#define var_debug(x) do { if (DEBUG) { debug_vprintf ("%s:%d:%s(): ", \
    __FILE__,  __LINE__, __func__); debug_vprintf x; }} while (0)

/* var_dump("%s" variable_name); */
#define var_dump(fmt, var) do { if (DEBUG) { debug_vprintf ("%s:%d:%s(): ", \
    __FILE__,  __LINE__, __func__); debug_vprintf ("%s = " fmt, #var, var); }} while (0)

#define DEBUG_HERE do { if (DEBUG) { debug_vprintf ("%s:%d:%s(): HERE\n", \
    __FILE__,  __LINE__, __func__); }} while (0)
Tom Hale
źródło
1

Korzystając z gcc, lubię:

#define DBGI(expr) ({int g2rE3=expr; fprintf(stderr, "%s:%d:%s(): ""%s->%i\n", __FILE__,  __LINE__, __func__, #expr, g2rE3); g2rE3;})

Ponieważ można go wstawić do kodu.

Załóżmy, że próbujesz debugować

printf("%i\n", (1*2*3*4*5*6));

720

Następnie możesz go zmienić na:

printf("%i\n", DBGI(1*2*3*4*5*6));

hello.c:86:main(): 1*2*3*4*5*6->720
720

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ę:

DBGI(printf("%i\n", DBGI(1*2*3*4*5*6)));

hello.c:86:main(): 1*2*3*4*5*6->720
720
hello.c:86:main(): printf("%i\n", DBGI(1*2*3*4*5*6))->4

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.

John Lawrence Aspden
źródło
1

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

  1. Musisz im zaufać, aby uzyskać optymalizację, co oczywiście MUSI się zdarzyć, jeśli masz wystarczający poziom optymalizacji.
  2. Co więcej, prawdopodobnie nie zrobią tego, jeśli utworzysz kompilację wydania z wyłączoną optymalizacją do celów testowych (co jest wprawdzie rzadkie); i prawie na pewno nie będą wcale podczas debugowania - wykonując dziesiątki lub setki instrukcji „if (DEBUG)” w czasie wykonywania; spowalniając w ten sposób wykonanie (co jest moim głównym zastrzeżeniem) i, co mniej ważne, zwiększając rozmiar pliku wykonywalnego lub biblioteki DLL; a zatem czasy wykonania i kompilacji. Jonathan jednak informuje mnie, że jego metoda polega na tym, aby w ogóle nie kompilować instrukcji.

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:

// FILE: DebugLog.h
// REMARKS: This is a generic pair of files useful for debugging.  It provides three levels of 
// debug logging, currently; in addition to disabling it.  Level 3 is the most information.
// Levels 2 and 1 have progressively more.  Thus, you can write: 
//     DEBUGLOG_LOG(1, "a number=%d", 7);
// and it will be seen if DEBUG is anything other than undefined or zero.  If you write
//     DEBUGLOG_LOG(3, "another number=%d", 15);
// it will only be seen if DEBUG is 3.  When not being displayed, these routines compile
// to NOTHING.  I reject the argument that debug code needs to always be compiled so as to 
// keep it current.  I would rather have a leaner and faster app, and just not be lazy, and 
// maintain debugs as needed.  I don't know if this works with the C preprocessor or not, 
// but the rest of the code is fully C compliant also if it is.

#define DEBUG 1

#ifdef DEBUG
#define DEBUGLOG_INIT(filename) debuglog_init(filename)
#else
#define debuglog_init(...)
#endif

#ifdef DEBUG
#define DEBUGLOG_CLOSE debuglog_close
#else
#define debuglog_close(...)
#endif

#define DEBUGLOG_LOG(level, fmt, ...) DEBUGLOG_LOG ## level (fmt, ##__VA_ARGS__)

#if DEBUG == 0
#define DEBUGLOG_LOG0(...)
#endif

#if DEBUG >= 1
#define DEBUGLOG_LOG1(fmt, ...) debuglog_log (fmt, ##__VA_ARGS__)
#else
#define DEBUGLOG_LOG1(...)
#endif

#if DEBUG >= 2
#define DEBUGLOG_LOG2(fmt, ...) debuglog_log (fmt, ##__VA_ARGS__)
#else
#define DEBUGLOG_LOG2(...)
#endif

#if DEBUG == 3
#define DEBUGLOG_LOG3(fmt, ...) debuglog_log (fmt, ##__VA_ARGS__)
#else
#define DEBUGLOG_LOG3(...)
#endif

void debuglog_init(char *filename);
void debuglog_close(void);
void debuglog_log(char* format, ...);

DebugLog.cpp:

// FILE: DebugLog.h
// REMARKS: This is a generic pair of files useful for debugging.  It provides three levels of 
// debug logging, currently; in addition to disabling it.  See DebugLog.h's remarks for more 
// info.

#include <stdio.h>
#include <stdarg.h>

#include "DebugLog.h"

FILE *hndl;
char *savedFilename;

void debuglog_init(char *filename)
{
    savedFilename = filename;
    hndl = fopen(savedFilename, "wt");
    fclose(hndl);
}

void debuglog_close(void)
{
    //fclose(hndl);
}

void debuglog_log(char* format, ...)
{
    hndl = fopen(savedFilename,"at");
    va_list argptr;
    va_start(argptr, format);
    vfprintf(hndl, format, argptr);
    va_end(argptr);
    fputc('\n',hndl);
    fclose(hndl);
}

Korzystanie z makr

Aby go użyć, po prostu wykonaj:

DEBUGLOG_INIT("afile.log");

Aby zapisać do pliku dziennika, po prostu wykonaj:

DEBUGLOG_LOG(1, "the value is: %d", anint);

Aby go zamknąć, wykonujesz:

DEBUGLOG_CLOSE();

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

#define DEBUG 1

Aby instrukcje logowania były kompilowane do zera, zrób

#define DEBUG 0

Jeśli potrzebujesz informacji z często wykonywanego fragmentu kodu (tj. Wysokiego poziomu szczegółowości), możesz napisać:

 DEBUGLOG_LOG(3, "the value is: %d", anint);

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.

  1. Można go rozszerzyć, aby zrobić tyle poziomów, ile chcesz.
  2. Kompiluje się do niczego, jeśli nie drukuje.
  3. Centralizuje IO w jednym łatwym do edycji miejscu.
  4. Jest elastyczny, używając formatowania printf.
  5. Ponownie, nie spowalnia to uruchamiania debugowania, podczas gdy zawsze kompilowane wydruki debugowania są zawsze wykonywane w trybie debugowania. Jeśli zajmujesz się informatyką, a pisanie przetwarzania informacji nie jest łatwiejsze, możesz znaleźć się w symulatorze obciążającym procesor, aby zobaczyć np. Gdzie debuger zatrzymuje go z indeksem poza zakresem dla wektora. Te działają już bardzo wolno w trybie debugowania. Obowiązkowe wykonanie setek wydruków debugowania z konieczności spowolni takie spowolnienie jeszcze bardziej. Dla mnie takie biegi nie są rzadkie.

Nie strasznie znaczące, ale dodatkowo:

  1. Drukowanie nie wymaga włamania bez argumentów (np. 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 #defines, 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 ./configurepoleceń 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.

CodeLurker
źródło
2
Mówisz „wykonywanie dziesiątek lub setek 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).
Jonathan Leffler,
Jonathan Leffler: Dziękuję za zwrócenie uwagi na moje błędne sformułowanie. Pozwoliłem, by moje myśli ścigały się szybciej niż palce, ciesząc się, że to się skończyło. Poprawiłem swoje obiekcje za pomocą „... 1) musisz im zaufać, aby uzyskać optymalizację, co oczywiście powinno się zdarzyć, jeśli masz wystarczający poziom optymalizacji. 2) Co więcej, nie zrobią tego, jeśli wydasz kompilację z optymalizacją wyłączono do celów testowych i prawdopodobnie nie będą wcale podczas debugowania - wykonując w ten sposób dziesiątki lub setki instrukcji „if (DEBUG)” w czasie wykonywania - zwiększając w ten sposób rozmiar pliku wykonywalnego lub biblioteki dll oraz czasy wykonania. ”
CodeLurker,
Aby wykonać inną ważną czynność, którą musisz wykonać, musisz mieć poziomy debugowania. Chociaż często nie potrzebuję wielu z nich włączonych, kilka aplikacji naprawdę korzysta z możliwości uzyskania wysokiego poziomu szczegółowości na temat pętli krytycznej czasowo za pomocą prostego „#define DEBUG 3”, a następnie powrotu do znacznie mniej szczegółowych informacji za pomocą „#define DEBUG 1”. Nigdy nie potrzebowałem więcej niż trzech poziomów, a co najmniej około 1/3 moich debugowania kompiluje się już w momencie wydania. Jeśli ostatnio używałem poziomu 3, prawdopodobnie WSZYSTKO tak.
CodeLurker
YMMV. Nowoczesny system, który pokazałem, obsługuje dynamiczne (środowisko wykonawcze) ustawienia poziomów debugowania, dzięki czemu możesz programowo decydować o tym, ile części debugowania jest wytwarzane w czasie wykonywania. Zwykle używałem poziomów 1-9, chociaż nie ma górnego limitu (lub dolnego limitu; domyślny poziom to 0, który zwykle jest wyłączony, ale można go wyraźnie zażądać podczas aktywnego programowania, jeśli to właściwe - nie jest odpowiedni do długotrwałej pracy). Wybrałem domyślny poziom 3; rzeczy można dostroić. To daje mi dużą kontrolę. Jeśli naprawdę nie chcesz testować kodu debugowania, gdy jest nieaktywny, zmień alternatywę na ((void)0)- to proste.
Jonathan Leffler
1
Ahh Pomogłoby to przeczytać całą rzecz. To dość długi post. Myślę, że jak dotąd ma to zasadnicze znaczenie. Okazuje się, że twoja, podobnie jak moja, może być używana do kompilacji lub nie kompilacji wszystkich wydruków debugowania i może obsługiwać poziomy; chociaż wprawdzie twoje mogą kompilować poziomy, których nie używasz - kosztem podczas debugowania.
CodeLurker,
0

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

#ifndef DEBUG_H
#define DEBUG_H

#define PRINT(DEBUG_CATEGORY, VALUE)  do { if (DEBUG_CATEGORY & DEBUG_MASK) Serial.print(VALUE);} while (0);

#endif

wywoływanie pliku .cpp

#define DEBUG_MASK 0x06
#include "Debug.h"

...
PRINT(4, "Time out error,\t");
...
użytkownik358795
źródło