#define makro untuk pencetakan debug di C?

209

Mencoba membuat makro yang dapat digunakan untuk mencetak pesan debug ketika DEBUG didefinisikan, seperti kode pseudo berikut:

#define DEBUG 1
#define debug_print(args ...) if (DEBUG) fprintf(stderr, args)

Bagaimana ini bisa dilakukan dengan makro?

jfarrell
sumber
Apakah kompiler (gcc) mengoptimalkan pernyataan seperti jika (DEBUG) {...} keluar, jika dalam kode produksi makro DEBUG diatur ke 0? Saya mengerti bahwa ada alasan bagus untuk membiarkan laporan debug terlihat oleh kompiler, tetapi perasaan buruk tetap ada. -Pat
Pat

Jawaban:

410

Jika Anda menggunakan kompiler C99 atau yang lebih baru

#define debug_print(fmt, ...) \
            do { if (DEBUG) fprintf(stderr, fmt, __VA_ARGS__); } while (0)

Itu mengasumsikan Anda menggunakan C99 (notasi daftar argumen variabel tidak didukung di versi sebelumnya). The do { ... } while (0)idiom memastikan bahwa kode tersebut bertindak seperti sebuah pernyataan (fungsi panggilan). Penggunaan kode tanpa syarat memastikan bahwa kompilator selalu memeriksa apakah kode debug Anda valid - tetapi pengoptimal akan menghapus kode ketika DEBUG adalah 0.

Jika Anda ingin bekerja dengan #ifdef DEBUG, ubah kondisi tes:

#ifdef DEBUG
#define DEBUG_TEST 1
#else
#define DEBUG_TEST 0
#endif

Dan kemudian gunakan DEBUG_TEST tempat saya menggunakan DEBUG.

Jika Anda bersikeras string literal untuk string format (mungkin ide yang bagus), Anda juga dapat memperkenalkan hal-hal seperti __FILE__, __LINE__dan __func__ke dalam output, yang dapat meningkatkan diagnostik:

#define debug_print(fmt, ...) \
        do { if (DEBUG) fprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, \
                                __LINE__, __func__, __VA_ARGS__); } while (0)

Ini bergantung pada penggabungan string untuk membuat string format yang lebih besar daripada yang ditulis programmer.

Jika Anda menggunakan kompiler C89

Jika Anda terjebak dengan C89 dan tidak ada ekstensi kompiler yang berguna, maka tidak ada cara yang sangat bersih untuk menanganinya. Teknik yang saya gunakan adalah:

#define TRACE(x) do { if (DEBUG) dbg_printf x; } while (0)

Dan kemudian, dalam kode, tulis:

TRACE(("message %d\n", var));

Tanda kurung ganda sangat penting - dan itulah sebabnya Anda memiliki notasi lucu dalam ekspansi makro. Seperti sebelumnya, kompiler selalu memeriksa kode untuk validitas sintaksis (yang baik) tetapi optimizer hanya memanggil fungsi pencetakan jika DEBUG makro mengevaluasi ke nol.

Ini memang membutuhkan fungsi dukungan - dbg_printf () dalam contoh - untuk menangani hal-hal seperti 'stderr'. Anda harus tahu cara menulis fungsi varargs, tetapi itu tidak sulit:

#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);
}

Anda juga dapat menggunakan teknik ini di C99, tetapi __VA_ARGS__teknik ini lebih rapi karena menggunakan notasi fungsi biasa, bukan hack kurung ganda.

Mengapa sangat penting bahwa kompilator selalu melihat kode debug?

[ Mengomel komentar dibuat untuk jawaban lain. ]

Satu ide sentral di balik implementasi C99 dan C89 di atas adalah bahwa compiler yang tepat selalu melihat pernyataan debug seperti printf. Ini penting untuk kode jangka panjang - kode yang akan bertahan satu atau dua dekade.

Misalkan sepotong kode sebagian besar tidak aktif (stabil) selama beberapa tahun, tetapi sekarang perlu diubah. Anda mengaktifkan kembali pelacakan debugging - tetapi itu membuat frustasi harus men-debug kode debugging (pelacakan) karena mengacu pada variabel yang telah diganti nama atau mengetik ulang, selama tahun-tahun pemeliharaan stabil. Jika kompiler (pasca-prosesor) selalu melihat pernyataan cetak, itu memastikan bahwa setiap perubahan di sekitarnya tidak membatalkan diagnostik. Jika kompiler tidak melihat pernyataan cetak, ia tidak dapat melindungi Anda dari kecerobohan Anda sendiri (atau kecerobohan kolega atau kolaborator Anda). Lihat ' Praktek Pemrograman ' oleh Kernighan dan Pike, khususnya Bab 8 (lihat juga Wikipedia tentang TPOP ).

Ini pengalaman 'sudah ada, selesai' - Saya pada dasarnya menggunakan teknik yang dijelaskan dalam jawaban lain di mana non-debug build tidak melihat pernyataan seperti printf selama beberapa tahun (lebih dari satu dekade). Tapi saya menemukan saran di TPOP (lihat komentar saya sebelumnya), dan kemudian mengaktifkan beberapa kode debug setelah beberapa tahun, dan mengalami masalah dengan perubahan konteks yang melanggar debugging. Beberapa kali, setelah pencetakan selalu divalidasi telah menyelamatkan saya dari masalah di kemudian hari.

Saya menggunakan NDEBUG untuk mengontrol pernyataan saja, dan makro terpisah (biasanya DEBUG) untuk mengontrol apakah penelusuran debug dibangun ke dalam program. Bahkan ketika penelusuran debug sudah ada di dalam, saya sering tidak ingin hasil debug muncul tanpa syarat, jadi saya memiliki mekanisme untuk mengontrol apakah output muncul (level debug, dan alih-alih memanggil fprintf()secara langsung, saya memanggil fungsi cetak debug yang hanya mencetak dengan syarat. sehingga kode yang sama dapat dicetak atau tidak dicetak berdasarkan opsi program). Saya juga memiliki versi kode 'multi-subsistem' untuk program yang lebih besar, sehingga saya dapat memiliki bagian yang berbeda dari program yang menghasilkan jumlah jejak yang berbeda - di bawah kendali runtime.

Saya menganjurkan bahwa untuk semua bangunan, kompiler harus melihat pernyataan diagnostik; Namun, kompiler tidak akan menghasilkan kode apa pun untuk pernyataan pelacakan debugging kecuali debug diaktifkan. Pada dasarnya, ini berarti bahwa semua kode Anda diperiksa oleh kompiler setiap kali Anda mengkompilasi - baik untuk rilis atau debugging. Ini hal yang bagus!

debug.h - versi 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 - versi 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 */

Varian argumen tunggal untuk C99 atau lebih baru

Kyle Brandt bertanya:

Pokoknya untuk melakukan ini debug_printtetap bekerja meskipun tidak ada argumen? Sebagai contoh:

    debug_print("Foo");

Ada satu hack sederhana dan kuno:

debug_print("%s\n", "Foo");

Solusi khusus-GCC yang ditunjukkan di bawah ini juga menyediakan dukungan untuk itu.

Namun, Anda dapat melakukannya dengan sistem C99 langsung dengan menggunakan:

#define debug_print(...) \
            do { if (DEBUG) fprintf(stderr, __VA_ARGS__); } while (0)

Dibandingkan dengan versi pertama, Anda kehilangan pemeriksaan terbatas yang memerlukan argumen 'fmt', yang berarti bahwa seseorang dapat mencoba memanggil 'debug_print ()' tanpa argumen (tetapi tanda koma dalam daftar argumen fprintf()tidak akan berhasil dikompilasi) . Apakah hilangnya pemeriksaan adalah masalah sama sekali masih bisa diperdebatkan.

Teknik khusus GCC untuk satu argumen

Beberapa kompiler dapat menawarkan ekstensi untuk cara lain dalam menangani daftar argumen panjang variabel dalam makro. Secara khusus, seperti yang pertama kali dicatat dalam komentar oleh Hugo Ideler , GCC memungkinkan Anda untuk menghilangkan koma yang biasanya akan muncul setelah argumen 'diperbaiki' terakhir ke makro. Ini juga memungkinkan Anda untuk menggunakan ##__VA_ARGS__dalam teks pengganti makro, yang menghapus koma sebelum notasi jika, tetapi hanya jika, token sebelumnya adalah koma:

#define debug_print(fmt, ...) \
            do { if (DEBUG) fprintf(stderr, fmt, ##__VA_ARGS__); } while (0)

Solusi ini mempertahankan manfaat dari memerlukan argumen format sambil menerima argumen opsional setelah format.

Teknik ini juga didukung oleh Dentang untuk kompatibilitas GCC.


Mengapa loop do-while?

Apa tujuan dari do whiledisini?

Anda ingin dapat menggunakan makro sehingga terlihat seperti pemanggilan fungsi, yang berarti akan diikuti oleh titik koma. Karena itu, Anda harus mengemas tubuh makro yang sesuai. Jika Anda menggunakan ifpernyataan tanpa di sekitarnya do { ... } while (0), Anda akan memiliki:

/* BAD - BAD - BAD */
#define debug_print(...) \
            if (DEBUG) fprintf(stderr, __VA_ARGS__)

Sekarang, anggaplah Anda menulis:

if (x > y)
    debug_print("x (%d) > y (%d)\n", x, y);
else
    do_something_useful(x, y);

Sayangnya, lekukan itu tidak mencerminkan kontrol aliran yang sebenarnya, karena preprosesor menghasilkan kode yang setara dengan ini (indentasi dan kurung kurawal ditambahkan untuk menekankan arti sebenarnya):

if (x > y)
{
    if (DEBUG)
        fprintf(stderr, "x (%d) > y (%d)\n", x, y);
    else
        do_something_useful(x, y);
}

Upaya makro selanjutnya mungkin:

/* BAD - BAD - BAD */
#define debug_print(...) \
            if (DEBUG) { fprintf(stderr, __VA_ARGS__); }

Dan fragmen kode yang sama sekarang menghasilkan:

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

Dan elsesekarang kesalahan sintaks. The do { ... } while(0)Menghindari lingkaran kedua masalah ini.

Ada satu cara lain untuk menulis makro yang mungkin berfungsi:

/* BAD - BAD - BAD */
#define debug_print(...) \
            ((void)((DEBUG) ? fprintf(stderr, __VA_ARGS__) : 0))

Ini meninggalkan fragmen program yang ditampilkan sebagai valid. Para (void)pemain mencegahnya digunakan dalam konteks di mana nilai diperlukan - tetapi itu dapat digunakan sebagai operan kiri dari operator koma di mana do { ... } while (0)versi tidak bisa. Jika Anda berpikir Anda harus dapat menanamkan kode debug ke dalam ekspresi seperti itu, Anda mungkin lebih suka ini. Jika Anda lebih suka mewajibkan cetak debug untuk bertindak sebagai pernyataan lengkap, maka do { ... } while (0)versinya lebih baik. Perhatikan bahwa jika tubuh makro melibatkan semi-titik dua (secara kasar), maka Anda hanya dapat menggunakan do { ... } while(0)notasi. Selalu berhasil; mekanisme pernyataan ekspresi bisa lebih sulit untuk diterapkan. Anda mungkin juga mendapatkan peringatan dari kompiler dengan formulir ekspresi yang Anda ingin hindari; itu akan tergantung pada kompiler dan flag yang Anda gunakan.


TPOP sebelumnya di http://plan9.bell-labs.com/cm/cs/tpop dan http://cm.bell-labs.com/cm/cs/tpop tetapi keduanya sekarang (2015-08-10) rusak.


Kode di GitHub

Jika Anda penasaran, Anda dapat melihat kode ini di GitHub di repositori SOQ (Stack Overflow Questions) saya sebagai file debug.c, debug.hdan mddebug.cdi sub-direktori src / libsoq .

Jonathan Leffler
sumber
1
Saya pikir GCC ## - pendekatan dari gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html akan layak untuk disebutkan di bawah judul "Satu argumen C99 varian".
Hugo Ideler
2
Bertahun-tahun kemudian, dan jawaban ini masih yang paling berguna dari semua internet, tentang cara mencetak alias! vfprintf tidak berfungsi di ruang kernel karena stdio tidak tersedia. Terima kasih! #define debug(...) \ do { if (DEBUG) \ printk("DRIVER_NAME:"); \ printk(__VA_ARGS__); \ printk("\n"); \ } while (0)
kevinf
6
Dalam contoh Anda dengan kata kunci __FILE__, __LINE__, __func__, __VA_ARGS__, itu tidak akan dikompilasi jika Anda tidak memiliki parameter printf, yaitu jika Anda hanya menelepon debug_print("Some msg\n"); Anda dapat memperbaikinya dengan menggunakan fprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, ##__VA_ARGS__); The ## __ VA_ARGS__ memungkinkan melewati tidak ada parameter ke fungsi.
mc_electron
1
@ LogicTom: perbedaannya adalah antara #define debug_print(fmt, ...)dan #define debug_print(...). Yang pertama membutuhkan setidaknya satu argumen, format string ( fmt) dan nol atau lebih argumen lainnya; yang kedua membutuhkan nol atau lebih argumen secara total. Jika Anda menggunakan debug_print()yang pertama, Anda mendapatkan kesalahan dari preprocessor tentang menyalahgunakan makro, sedangkan yang kedua tidak. Namun, Anda masih mendapatkan kesalahan kompilasi karena teks pengganti tidak valid C. Jadi, sebenarnya tidak banyak perbedaan - karenanya penggunaan istilah 'pengecekan terbatas'.
Jonathan Leffler
1
Varian yang ditunjukkan di atas, @ St.Antario, menggunakan level debugging aktif tunggal di seluruh aplikasi, dan saya biasanya menggunakan opsi baris perintah untuk memungkinkan level debugging diatur ketika program dijalankan. Saya juga memiliki varian yang mengenali beberapa subsistem yang berbeda, yang masing-masing diberi nama dan level debugnya sendiri, sehingga saya dapat menggunakan -D input=4,macros=9,rules=2untuk mengatur level debug dari sistem input ke 4, sistem makro ke 9 (menjalani pengawasan ketat) ) dan sistem aturan ke 2. Ada variasi yang tak ada habisnya pada tema; gunakan apa pun yang cocok untukmu.
Jonathan Leffler
28

Saya menggunakan sesuatu seperti ini:

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

Daripada saya hanya menggunakan D sebagai awalan:

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

Compiler melihat kode debug, tidak ada masalah koma dan bekerja di mana-mana. Juga berfungsi ketika printftidak cukup, katakanlah ketika Anda harus membuang array atau menghitung beberapa nilai diagnosa yang berlebihan untuk program itu sendiri.

EDIT: Ok, ini mungkin menghasilkan masalah ketika ada elsesuatu tempat dekat yang dapat dicegat oleh suntikan ini if. Ini adalah versi yang membahasnya:

#ifdef DEBUG
 #define D 
#else
 #define D for(;0;)
#endif
mbq
sumber
3
Adapun for(;0;), mungkin menghasilkan masalah ketika Anda menulis sesuatu seperti D continue;atau D break;.
ACcreator
1
Dapatkan saya; tampaknya sangat tidak mungkin terjadi secara kebetulan.
mbq
11

Untuk implementasi portabel (ISO C90), Anda bisa menggunakan tanda kurung ganda, seperti ini;

#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;
}

atau (retas, tidak akan merekomendasikannya)

#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
sumber
3
@ LB: untuk membuat preprocessor `think 'hanya ada satu argumen, sementara membiarkan _ untuk diperluas pada tahap selanjutnya.
Marcin Koziuk
10

Inilah versi yang saya gunakan:

#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
sumber
9

Saya akan melakukan sesuatu seperti

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

Saya pikir ini lebih bersih.

LB40
sumber
Saya tidak terlalu suka ide menggunakan makro di dalam tes sebagai bendera. Bisakah Anda menjelaskan mengapa pencetakan debug harus selalu diperiksa?
LB40
1
@ Jonathan: Jika kode hanya dieksekusi dalam mode debug, mengapa Anda harus peduli jika dikompilasi dalam mode non-debug? assert()dari stdlib bekerja dengan cara yang sama dan saya biasanya hanya kembali menggunakan NDEBUGmakro untuk kode debug saya sendiri ...
Christoph
menggunakan DEBUG dalam pengujian, jika seseorang melakukan DEBUG undef yang tidak terkontrol, kode Anda tidak lagi dikompilasi. Baik ?
LB40
4
Sangat frustasi untuk mengaktifkan debugging dan kemudian harus men-debug kode debugging karena mengacu pada variabel yang telah diganti nama atau mengetik ulang, dll. Jika kompiler (post pre-processor) selalu melihat pernyataan cetak, itu memastikan bahwa setiap perubahan di sekitarnya memiliki tidak membatalkan diagnostik. Jika kompiler tidak melihat pernyataan cetak, ia tidak dapat melindungi Anda dari kecerobohan Anda sendiri (atau kecerobohan kolega atau kolaborator Anda). Lihat 'Praktek Pemrograman' oleh Kernighan dan Pike - plan9.bell-labs.com/cm/cs/tpop .
Jonathan Leffler
1
@ Christoph: well, semacam ... Saya menggunakan NDEBUG untuk mengontrol pernyataan saja, dan makro terpisah (biasanya DEBUG) untuk mengontrol penelusuran debug. Saya sering tidak ingin output debug muncul tanpa syarat, jadi saya punya mekanisme untuk mengontrol apakah output muncul (level debug, dan alih-alih memanggil fprintf () secara langsung, saya memanggil fungsi cetak debug yang hanya mencetak dengan syarat dan kondisi yang sama sehingga build yang sama dari kode dapat dicetak atau tidak dicetak berdasarkan opsi program). Saya menganjurkan bahwa untuk semua bangunan, kompiler harus melihat pernyataan diagnostik; Namun, itu tidak akan menghasilkan kode kecuali debug diaktifkan.
Jonathan Leffler
8

Menurut http://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html , harus ada ##sebelumnya __VA_ARGS__.

Jika tidak, makro #define dbg_print(format, ...) printf(format, __VA_ARGS__)tidak akan mengkompilasi contoh berikut: dbg_print("hello world");.

Chobits Tai
sumber
1
Selamat datang di Stack Overflow. Anda benar bahwa GCC memiliki ekstensi non-standar yang Anda referensi. Jawaban yang saat ini diterima sebenarnya menyebutkan ini, termasuk persis URL referensi yang Anda berikan.
Jonathan Leffler
7
#define debug_print(FMT, ARGS...) do { \
    if (DEBUG) \
        fprintf(stderr, "%s:%d " FMT "\n", __FUNCTION__, __LINE__, ## ARGS); \
    } while (0)
eyalm
sumber
Versi C mana yang mendukung notasi itu? Dan, jika itu berhasil, token menempelkan semua argumen seperti itu berarti bahwa Anda hanya memiliki satu set pilihan yang sangat terbatas untuk string format, bukan?
Jonathan Leffler
@ Jonathan: gcc (Debian 4.3.3-13) 4.3.3
eyalm
1
OK - setuju: ini didokumentasikan sebagai ekstensi GNU lama (bagian 5.17 dari manual GCC 4.4.1). Tetapi Anda mungkin harus mendokumentasikan bahwa itu hanya akan bekerja dengan GCC - atau mungkin kami telah melakukannya di antara kami di komentar ini.
Jonathan Leffler
1
Niat saya adalah untuk menunjukkan gaya lain menggunakan args dan terutama untuk menunjukkan penggunaan FUNCTION dan LINE
eyalm
2

Inilah yang saya gunakan:

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

Ini memiliki manfaat yang bagus untuk menangani printf dengan benar, bahkan tanpa argumen tambahan. Dalam kasus DBG == 0, bahkan kompiler paling bodoh tidak dapat mengunyah, jadi tidak ada kode yang dihasilkan.

5tenzel
sumber
Lebih baik memiliki kompilator selalu memeriksa kode debug.
Jonathan Leffler
1

Favorit saya di bawah ini adalah var_dump, yang ketika disebut sebagai:

var_dump("%d", count);

menghasilkan output seperti:

patch.c:150:main(): count = 0

Kredit ke @ "Jonathan Leffler". Semua C89-senang:

Kode

#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
sumber
1

Jadi, saat menggunakan gcc, saya suka:

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

Karena itu bisa dimasukkan ke dalam kode.

Misalkan Anda mencoba melakukan debug

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

720

Kemudian Anda dapat mengubahnya ke:

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

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

Dan Anda bisa mendapatkan analisis ekspresi apa yang dievaluasi untuk apa.

Ini dilindungi terhadap masalah evaluasi ganda, tetapi tidak adanya gensim membiarkannya terbuka untuk tabrakan nama.

Namun ia bersarang:

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

Jadi saya pikir selama Anda menghindari menggunakan g2rE3 sebagai nama variabel, Anda akan baik-baik saja.

Tentu saja saya telah menemukannya (dan versi sekutu untuk string, dan versi untuk level debug dll) sangat berharga.

John Lawrence Aspden
sumber
1

Saya telah memikirkan cara melakukan ini selama bertahun-tahun, dan akhirnya menemukan solusi. Namun, saya tidak tahu bahwa sudah ada solusi lain di sini. Pertama, berbeda dengan jawaban Leffler , saya tidak melihat argumennya bahwa cetakan debug harus selalu dikompilasi. Saya lebih suka tidak memiliki banyak kode yang tidak perlu dieksekusi di proyek saya, ketika tidak diperlukan, dalam kasus di mana saya perlu menguji dan mereka mungkin tidak dioptimalkan.

Tidak mengkompilasi setiap kali mungkin terdengar lebih buruk daripada dalam praktik sebenarnya. Anda benar-benar berakhir dengan cetakan debug yang kadang-kadang tidak dikompilasi, tetapi tidak terlalu sulit untuk mengkompilasi dan mengujinya sebelum menyelesaikan proyek. Dengan sistem ini, jika Anda menggunakan tiga level debug, letakkan saja di pesan debug level tiga, perbaiki kesalahan kompilasi Anda dan periksa yang lainnya sebelum Anda menyelesaikan kode Anda. (Karena tentu saja, penyusunan laporan debug bukan jaminan bahwa mereka masih berfungsi sebagaimana dimaksud.)

Solusi saya juga menyediakan level detail debug; dan jika Anda mengaturnya ke level tertinggi, semuanya dikompilasi. Jika Anda telah menggunakan level detail debug tinggi baru-baru ini, mereka semua dapat melakukan kompilasi pada saat itu. Pembaruan akhir harus cukup mudah. Saya tidak pernah membutuhkan lebih dari tiga level, tetapi Jonathan mengatakan dia menggunakan sembilan level. Metode ini (seperti Leffler) dapat diperluas ke sejumlah level. Penggunaan metode saya mungkin lebih sederhana; hanya membutuhkan dua pernyataan saat digunakan dalam kode Anda. Saya, bagaimanapun, mengkode makro TUTUP juga - meskipun tidak melakukan apa-apa. Mungkin jika saya mengirim ke file.

Terhadap biaya langkah ekstra menguji mereka untuk melihat bahwa mereka akan dikompilasi sebelum pengiriman, adalah itu

  1. Anda harus percaya mereka untuk dioptimalkan, yang memang HARUS terjadi jika Anda memiliki tingkat optimisasi yang memadai.
  2. Selain itu, mereka mungkin tidak akan melakukannya jika Anda membuat kompilasi rilis dengan optimisasi dimatikan untuk tujuan pengujian (yang memang jarang); dan mereka hampir pasti tidak akan sama sekali selama debug - sehingga mengeksekusi lusinan atau ratusan pernyataan "jika (DEBUG)" pada saat runtime; sehingga memperlambat eksekusi (yang merupakan keberatan prinsip saya) dan yang kurang penting, meningkatkan ukuran executable atau dll Anda; dan karenanya waktu eksekusi dan kompilasi. Namun, Jonathan memberi tahu saya bahwa metodenya dapat dibuat untuk tidak mengkompilasi pernyataan sama sekali.

Cabang sebenarnya relatif cukup mahal dalam prosesor pre-fetching modern. Mungkin bukan masalah besar jika aplikasi Anda tidak kritis terhadap waktu; tetapi jika kinerja merupakan masalah, maka, ya, masalah yang cukup besar yang saya lebih suka untuk memilih kode debug yang agak lebih cepat dieksekusi (dan mungkin rilis lebih cepat, dalam kasus yang jarang, seperti yang disebutkan).

Jadi, yang saya inginkan adalah makro cetak debug yang tidak mengkompilasi jika tidak akan dicetak, tetapi tidak jika itu dicetak. Saya juga ingin level debugging, jadi, misalnya jika saya ingin bagian kode yang sangat penting kinerja tidak untuk mencetak pada beberapa waktu, tetapi untuk mencetak pada yang lain, saya bisa mengatur level debug, dan meminta tambahan cetakan debug. Saya menemukan cara untuk menerapkan level debug yang menentukan apakah cetakan bahkan dikompilasi atau tidak. Saya mencapainya dengan cara ini:

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);
}

Menggunakan makro

Untuk menggunakannya, lakukan saja:

DEBUGLOG_INIT("afile.log");

Untuk menulis ke file log, cukup lakukan:

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

Untuk menutupnya, Anda harus:

DEBUGLOG_CLOSE();

walaupun saat ini ini bahkan tidak perlu, secara teknis, karena tidak melakukan apa-apa. Saya masih menggunakan TUTUP sekarang, namun, jika saya berubah pikiran tentang cara kerjanya, dan ingin membiarkan file terbuka di antara pernyataan logging.

Kemudian, ketika Anda ingin mengaktifkan pencetakan debug, cukup edit #define pertama dalam file header untuk mengatakan, misalnya

#define DEBUG 1

Agar laporan logging tidak dikompilasi, lakukan

#define DEBUG 0

Jika Anda memerlukan info dari sepotong kode yang sering dieksekusi (yaitu detail tingkat tinggi), Anda mungkin ingin menulis:

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

Jika Anda mendefinisikan DEBUG menjadi 3, masuk ke kompilasi level 1, 2 & 3. Jika Anda mengaturnya ke 2, Anda mendapatkan logging level 1 & 2. Jika Anda mengaturnya ke 1, Anda hanya mendapatkan pernyataan logging level 1.

Mengenai loop do-while, karena ini mengevaluasi salah satu fungsi tunggal atau tidak sama sekali, alih-alih pernyataan if, loop tidak diperlukan. OK, kata saya untuk menggunakan C daripada C ++ IO (dan QString :: arg () Qt adalah cara yang lebih aman untuk memformat variabel ketika di Qt, juga - itu cukup apik, tetapi membutuhkan lebih banyak kode dan dokumentasi pemformatan tidak terorganisir mungkin - tetapi masih saya telah menemukan kasus-kasus yang lebih disukai), tetapi Anda dapat memasukkan kode apa pun dalam file .cpp yang Anda inginkan. Ini juga mungkin sebuah kelas, tetapi kemudian Anda harus membuat instance dan mengikutinya, atau melakukan yang baru () dan menyimpannya. Dengan cara ini, Anda cukup memasukkan #include, init, dan secara opsional menutup pernyataan ke sumber Anda, dan Anda siap untuk mulai menggunakannya. Akan tetapi, itu akan membuat kelas yang bagus, jika Anda cenderung.

Saya sebelumnya telah melihat banyak solusi, tetapi tidak ada yang cocok dengan kriteria saya dan juga yang ini.

  1. Dapat diperluas untuk melakukan level sebanyak yang Anda suka.
  2. Itu tidak bisa dikompilasi jika tidak mencetak.
  3. Ini memusatkan IO di satu tempat yang mudah diedit.
  4. Ini fleksibel, menggunakan format printf.
  5. Sekali lagi, ini tidak memperlambat proses debug, sedangkan cetakan debug yang selalu dikompilasi selalu dieksekusi dalam mode debug. Jika Anda melakukan ilmu komputer, dan tidak mudah untuk menulis pemrosesan informasi, Anda mungkin menemukan diri Anda menjalankan simulator yang menggunakan CPU, untuk melihat misalnya di mana debugger menghentikannya dengan indeks di luar jangkauan untuk vektor. Ini berjalan ekstra lambat dalam mode debug. Eksekusi wajib dari ratusan cetakan debug akan memperlambat berjalan seperti itu bahkan lebih jauh. Bagi saya, menjalankan seperti itu tidak jarang.

Tidak terlalu signifikan, tetapi juga:

  1. Tidak memerlukan retasan untuk mencetak tanpa argumen (misalnya DEBUGLOG_LOG(3, "got here!");); sehingga memungkinkan Anda untuk menggunakan, misalnya pemformatan Qt .arg () yang lebih aman. Ini bekerja pada MSVC, dan karenanya, mungkin gcc. Ini digunakan ##dalam #defines, yang non-standar, seperti yang ditunjukkan Leffler, tetapi didukung secara luas. (Anda dapat mengode ulang untuk tidak digunakan ##jika perlu, tetapi Anda harus menggunakan peretasan seperti yang disediakannya.)

Peringatan: Jika Anda lupa untuk memberikan argumen tingkat pencatatan, MSVC tidak mengklaim klaim pengidentifikasi tidak didefinisikan.

Anda mungkin ingin menggunakan nama simbol preprosesor selain DEBUG, karena beberapa sumber juga mendefinisikan simbol itu (mis. Prog menggunakan ./configureperintah untuk mempersiapkan bangunan). Rasanya alami bagi saya ketika saya mengembangkannya. Saya mengembangkannya dalam aplikasi di mana DLL sedang digunakan oleh sesuatu yang lain, dan lebih baik untuk mengirim cetakan log ke file; tetapi mengubahnya ke vprintf () akan bekerja dengan baik juga.

Saya harap ini menyelamatkan banyak dari Anda yang berduka karena mencari tahu cara terbaik untuk melakukan debug logging; atau menunjukkan satu yang Anda sukai. Dengan setengah hati aku berusaha memikirkan hal ini selama beberapa dekade. Bekerja di MSVC 2012 & 2015, dan karenanya mungkin pada gcc; dan mungkin bekerja pada banyak orang lain, tetapi saya belum mengujinya pada mereka.

Saya bermaksud membuat versi streaming untuk satu hari ini juga.

Catatan: Terima kasih kepada Leffler, yang dengan ramah membantu saya memformat pesan saya lebih baik untuk StackOverflow.

CodeLurker
sumber
2
Anda mengatakan "mengeksekusi lusinan atau ratusan if (DEBUG)pernyataan saat runtime, yang tidak dioptimalkan" - yang miring pada kincir angin . Inti dari sistem yang saya jelaskan adalah bahwa kode tersebut akan diperiksa oleh kompilator (penting, dan otomatis - tidak ada membangun khusus yang dibutuhkan) tetapi kode debug tidak dihasilkan sama sekali karena yang dioptimalkan keluar (sehingga ada nol runtime dampak pada ukuran atau kinerja kode karena kode tidak ada pada saat runtime).
Jonathan Leffler
Jonathan Leffler: Terima kasih telah menunjukkan kekeliruan saya. Aku membiarkan pikiranku berpacu lebih cepat dari jari-jariku, sangat senang telah selesai. Saya telah merevisi keberatan saya dengan "... 1) Anda harus mempercayai mereka untuk dioptimalkan, yang memang harus terjadi jika Anda memiliki tingkat optimisasi yang memadai. 2) Selain itu, mereka tidak akan melakukannya jika Anda membuat rilis kompilasi dengan optimisasi. dimatikan untuk tujuan pengujian; dan mereka mungkin tidak akan sama sekali selama debug - sehingga mengeksekusi lusinan atau ratusan pernyataan 'jika (DEBUG)' pada saat runtime - dengan demikian meningkatkan ukuran executable atau dll Anda, dan waktu eksekusi. "
CodeLurker
Agar Anda dapat melakukan hal penting lain yang saya lakukan, Anda harus memiliki level debug. Meskipun saya sering tidak membutuhkan banyak dari mereka dihidupkan, beberapa aplikasi benar-benar mendapat manfaat dari bisa mendapatkan tingkat detail yang besar tentang perputaran waktu-kritis dengan "#define DEBUG 3" sederhana, dan kemudian kembali ke apalagi informasi yang bertele-tele dengan "#definisikan DEBUG 1". Saya tidak pernah membutuhkan lebih dari tiga level, dan dengan demikian, setidaknya sekitar 1/3 dari debitur saya sudah dikompilasi. Jika saya menggunakan level 3 baru-baru ini, mereka mungkin SEMUA melakukannya.
CodeLurker
YMMV. Sistem modern yang saya tunjukkan mendukung pengaturan dinamis (runtime) tingkat debug, sehingga Anda dapat memutuskan secara program berapa banyak debug yang dihasilkan saat runtime. Saya biasanya menggunakan level 1-9, meskipun tidak ada batas atas (atau batas bawah; tingkat default adalah 0 yang biasanya mati, tetapi dapat secara eksplisit diminta selama pengembangan aktif jika sesuai - tidak sesuai untuk pekerjaan jangka panjang). Saya memilih level default 3; semuanya bisa disetel. Ini memberi saya banyak kontrol. Jika Anda benar-benar tidak ingin menguji kode debug ketika tidak aktif, ubah alternatifnya menjadi ((void)0)- mudah.
Jonathan Leffler
1
Ahh. Itu akan membantu untuk membaca semuanya. Ini adalah posting yang agak panjang. Saya pikir itu punya poin penting sejauh ini. Ternyata milik Anda, seperti milik saya, dapat digunakan untuk mengkompilasi atau tidak mengkompilasi semua cetakan debug, dan dapat mendukung level; walaupun harus diakui, Anda dapat mengkompilasi level yang tidak Anda gunakan - dengan biaya selama debug.
CodeLurker
0

Saya percaya variasi tema ini memberikan kategori debug tanpa perlu memiliki nama makro yang terpisah per kategori.

Saya menggunakan variasi ini dalam proyek Arduino di mana ruang program dibatasi hingga 32K dan memori dinamis terbatas pada 2K. Penambahan pernyataan debug dan jejak string debug dengan cepat menghabiskan ruang. Jadi sangat penting untuk dapat membatasi jejak debug yang disertakan pada waktu kompilasi ke minimum yang diperlukan setiap kali kode dibangun.

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

memanggil file .cpp

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

...
PRINT(4, "Time out error,\t");
...
user358795
sumber