#define макрос для отладочной печати в C?


209

Попытка создать макрос, который можно использовать для отладочных сообщений печати, когда определен DEBUG, например, следующий псевдокод:

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

Как это достигается с помощью макроса?


Будет ли компилятор (gcc) оптимизировать операторы наподобие if (DEBUG) {...} out, если в рабочем коде макрос DEBUG установлен в 0? Я понимаю, что есть веские причины оставлять отладочные операторы видимыми для компилятора, но плохое предчувствие остается. -Пат
Пэт

Ответы:


410

Если вы используете компилятор C99 или новее

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

Предполагается, что вы используете C99 (нотация списка переменных аргумента не поддерживается в более ранних версиях). В do { ... } while (0)идиома гарантирует , что код действует как (вызов функции) заявление. Безусловное использование кода гарантирует, что компилятор всегда проверяет правильность вашего кода отладки - но оптимизатор удалит код, когда DEBUG равен 0.

Если вы хотите работать с #ifdef DEBUG, измените условие теста:

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

А затем используйте DEBUG_TEST, где я использовал DEBUG.

Если вы настаиваете на строковый литерал для строки формата (вероятно, хорошая идея в любом случае), вы можете также ввести такие вещи , как __FILE__, __LINE__и __func__в продукции, которая может улучшить диагностику:

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

Это основано на конкатенации строк для создания строки большего формата, чем пишет программист.

Если вы используете компилятор C89

Если вы застряли с C89 и не имеете полезного расширения компилятора, то нет особо чистого способа справиться с этим. Техника, которую я использовал, была:

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

А затем в коде напишите:

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

Двойные скобки имеют решающее значение - и именно поэтому у вас есть забавные обозначения в расширении макроса. Как и прежде, компилятор всегда проверяет код на синтаксическую достоверность (что хорошо), но оптимизатор вызывает функцию печати только в том случае, если макрос DEBUG оценивает ненулевое значение.

Для этого требуется поддержка функции - dbg_printf () в примере - для обработки таких вещей, как 'stderr'. Требуется, чтобы вы знали, как писать функции varargs, но это не сложно:

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

Конечно, вы также можете использовать эту технику в C99, но эта __VA_ARGS__техника более изящна, потому что она использует обычную функцию обозначения, а не хак с двойными скобками.

Почему так важно, чтобы компилятор всегда видел код отладки?

[ Перефразирование комментариев к другому ответу. ]

Одна центральная идея, лежащая в основе описанных выше реализаций C99 и C89, заключается в том, что сам компилятор всегда видит отладочные операторы типа printf. Это важно для долгосрочного кода - кода, который будет длиться десять или два года.

Предположим, что часть кода была в основном неактивной (стабильной) в течение ряда лет, но теперь ее необходимо изменить. Вы снова активируете трассировку отладки, но разочаровывает необходимость отладки кода отладки (трассировки), поскольку он ссылается на переменные, которые были переименованы или перепечатаны в годы стабильного обслуживания. Если компилятор (постпроцессор) всегда видит оператор печати, он гарантирует, что любые окружающие изменения не сделали диагностику недействительной. Если компилятор не видит оператор печати, он не может защитить вас от вашей собственной небрежности (или небрежности ваших коллег или сотрудников). См. « Практику программирования » Кернигана и Пайка, особенно главу 8 (см. Также Википедию по TPOP ).

Это был опыт «сделано, сделано так» - я использовал в основном технику, описанную в других ответах, где не отладочная сборка не видит подобные printf операторы в течение ряда лет (более десяти лет). Но я натолкнулся на совет в TPOP (см. Мой предыдущий комментарий), а затем через несколько лет включил некоторый код отладки и столкнулся с проблемами изменения контекста, нарушающими отладку. Несколько раз проверка печати всегда спасала меня от дальнейших проблем.

Я использую NDEBUG только для контроля утверждений и отдельный макрос (обычно DEBUG) для контроля встроенной трассировки отладки в программу. Даже когда встроена трассировка отладки, я часто не хочу, чтобы выходные данные отладки появлялись безоговорочно, поэтому у меня есть механизм для контроля появления выходных данных (уровни отладки, и вместо fprintf()прямого вызова я вызываю функцию печати отладки, которая печатает только условно поэтому одна и та же сборка кода может печатать или не печатать в зависимости от параметров программы). У меня также есть версия кода с несколькими подсистемами для больших программ, так что у меня могут быть разные разделы программы, производящие различное количество трассировки - под контролем времени выполнения.

Я защищаю то, что для всех сборок компилятор должен видеть диагностические утверждения; однако компилятор не будет генерировать код для операторов трассировки отладки, если не включена отладка. По сути, это означает, что весь ваш код проверяется компилятором каждый раз, когда вы компилируете - для выпуска или отладки. Это хорошая вещь!

debug.h - версия 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 - версия 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 */

Вариант с одним аргументом для C99 или позже

Кайл Брандт спросил:

В любом случае, чтобы сделать это, debug_printвсе еще работает, даже если нет аргументов? Например:

    debug_print("Foo");

Есть один простой старомодный хак:

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

Решение только для GCC, показанное ниже, также поддерживает это.

Тем не менее, вы можете сделать это с прямой системой C99, используя:

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

По сравнению с первой версией, вы теряете ограниченную проверку, которая требует аргумента 'fmt', что означает, что кто-то может попытаться вызвать 'debug_print ()' без аргументов (но завершающая запятая в списке аргументов fprintf()не будет компилироваться) , Если потери проверки является проблемой вообще спорно.

GCC-специфическая техника для одного аргумента

Некоторые компиляторы могут предлагать расширения для других способов обработки списков аргументов переменной длины в макросах. В частности, как впервые отмечено в комментариях Хьюго Иделера , GCC позволяет вам пропустить запятую, которая обычно появляется после последнего «фиксированного» аргумента макроса. Это также позволяет вам использовать ##__VA_ARGS__в тексте замены макроса, который удаляет запятую, предшествующую нотации, если, но только если предыдущий токен является запятой:

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

Это решение сохраняет преимущество требования аргумента формата при принятии необязательных аргументов после формата.

Этот метод также поддерживается Clang для совместимости с GCC.


Почему цикл do-while?

Какова цель do whileздесь?

Вы хотите иметь возможность использовать макрос так, чтобы он выглядел как вызов функции, что означает, что за ним следует точка с запятой. Поэтому вы должны упаковать тело макроса в соответствии с вашими требованиями. Если вы используете ifутверждение без окружения do { ... } while (0), у вас будет:

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

Теперь предположим, что вы пишете:

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

К сожалению, этот отступ не отражает фактического управления потоком, потому что препроцессор производит код, эквивалентный этому (отступ и скобки добавлены, чтобы подчеркнуть фактическое значение):

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

Следующая попытка макроса может быть:

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

И тот же фрагмент кода теперь производит:

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

И elseтеперь это синтаксическая ошибка. В do { ... } while(0)избегает цикла обе эти проблемы.

Есть еще один способ написания макроса, который может работать:

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

Это оставляет фрагмент программы, показанный как действительный. Приведение (void)запрещает его использование в контекстах, где требуется значение, но его можно использовать в качестве левого операнда оператора запятой, где do { ... } while (0)версия не может. Если вы считаете, что сможете встраивать отладочный код в такие выражения, вы можете предпочесть это. Если вы предпочитаете, чтобы отладочная печать действовала как полный оператор, то do { ... } while (0)версия лучше. Обратите внимание, что если в теле макроса используются точки с запятой (грубо говоря), то вы можете использовать только do { ... } while(0)обозначения. Это всегда работает; механизм выражения выражения может быть более сложным для применения. Вы также можете получить предупреждения от компилятора с формой выражения, которую вы предпочитаете избегать; это будет зависеть от компилятора и используемых вами флагов.


Ранее TPOP был на http://plan9.bell-labs.com/cm/cs/tpop и http://cm.bell-labs.com/cm/cs/tpop, но оба сейчас (2015-08-10) сломана.


Код в GitHub

Если вам интересно, вы можете посмотреть на этот код в GitHub в моих SOQ (Stack Overflow) Вопросы хранилища в виде файлов debug.c, debug.hи mddebug.cв SRC / libsoq подкаталоге.


1
Я думаю, что GCC ## - подход из gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html стоит упомянуть под заголовком «Вариант C99 с одним аргументом».
Хьюго Иделер

2
Годы спустя, и этот ответ по-прежнему самый полезный из всех интернетов, о том, как псевдоним printk! vfprintf не работает в пространстве ядра, так как stdio недоступен. Спасибо! #define debug(...) \ do { if (DEBUG) \ printk("DRIVER_NAME:"); \ printk(__VA_ARGS__); \ printk("\n"); \ } while (0)
Кевинф

6
В вашем примере с ключевыми словами __FILE__, __LINE__, __func__, __VA_ARGS__он не скомпилируется, если у вас нет параметров printf, т.е. если вы просто вызовете. debug_print("Some msg\n"); Это можно исправить с помощью fprintf(stderr, "%s:%d:%s(): " fmt, __FILE__, __LINE__, __func__, ##__VA_ARGS__);## __ VA_ARGS__, позволяющего не передавать параметры в функцию.
mc_electron

1
@LogicTom: разница между #define debug_print(fmt, ...)и #define debug_print(...). Первый из них требует как минимум один аргумент, формат строки ( fmt) и ноль или более других аргументов; для второго требуется ноль или более аргументов. Если вы используете debug_print()с первым, вы получите ошибку от препроцессора о неправильном использовании макроса, в то время как второе - нет. Тем не менее, вы по-прежнему получаете ошибки компиляции, потому что текст замены не является допустимым C. Таким образом, на самом деле это не большая разница - отсюда и использование термина «ограниченная проверка».
Джонатан Леффлер

1
Вариант, показанный выше, @ St.Antario, использует один активный уровень отладки для всего приложения, и я обычно использую параметры командной строки, чтобы разрешить установку уровня отладки при запуске программы. У меня также есть вариант, который распознает несколько разных подсистем, каждой из которых присваивается имя и свой собственный уровень отладки, так что я могу использовать -D input=4,macros=9,rules=2для установки уровня отладки системы ввода на 4, системы макросов на 9 (подвергается тщательному анализу). ) и система правил до 2. Существуют бесконечные вариации на тему; используйте то, что вам подходит.
Джонатан Леффлер

28

Я использую что-то вроде этого:

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

Чем я просто использую D в качестве префикса:

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

Компилятор видит код отладки, проблем с запятыми нет, и он работает везде. Кроме того, он работает, когда printfэтого недостаточно, например, когда необходимо вывести массив или вычислить некоторое диагностическое значение, которое является избыточным для самой программы.

РЕДАКТИРОВАТЬ: Хорошо, это может вызвать проблему, когда есть elseгде-то рядом, которые могут быть перехвачены этим введено if. Это версия, которая проходит через это:

#ifdef DEBUG
 #define D 
#else
 #define D for(;0;)
#endif

3
Что касается for(;0;), это может вызвать проблемы, когда вы пишете что-то вроде D continue;или D break;.
ACcreator

1
Подловил; тем не менее, кажется маловероятным, что это может произойти случайно.
МБк

11

Для переносимой (ISO C90) реализации вы можете использовать двойные скобки, например так;

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

или (хак, не рекомендую)

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

3
@LB: чтобы заставить препроцессор «думать», есть только один аргумент, и позволить _ быть расширенным на более позднем этапе.
Марчин Козюк

10

Вот версия, которую я использую:

#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

9

Я бы сделал что-то вроде

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

Я думаю, что это чище.


Мне не очень нравится идея использовать макрос внутри теста в качестве флага. Не могли бы вы объяснить, почему отладочная печать должна всегда проверяться?
LB40

1
@Jonathan: Если код выполняется только в режиме отладки, почему вас должно волновать, компилируется ли он в режиме без отладки? assert()из stdlib работает так же, и я обычно просто повторно использую NDEBUGмакрос для моего собственного кода отладки ...
Кристоф

используя DEBUG в тесте, если кто-то делает неконтролируемый undef DEBUG, ваш код больше не компилируется. право ?
LB40

4
Огорчает включение отладки, а затем приходится отлаживать код отладки, потому что он ссылается на переменные, которые были переименованы или перепечатаны и т. Д. Если компилятор (постпроцессор) всегда видит оператор печати, он гарантирует, что любые окружающие изменения имеют Не признана недействительной диагностика. Если компилятор не видит оператор печати, он не может защитить вас от вашей собственной небрежности (или небрежности ваших коллег или сотрудников). Смотрите «Практика программирования» Кернигана и Пайка - plan9.bell-labs.com/cm/cs/tpop .
Джонатан Леффлер

1
@Christoph: ну вроде ... Я использую NDEBUG только для контроля утверждений и отдельный макрос (обычно DEBUG) для управления трассировкой отладки. Я часто не хочу, чтобы выходные данные отладки появлялись безоговорочно, поэтому у меня есть механизм, чтобы контролировать, появляется ли выход (уровни отладки, и вместо прямого вызова fprintf (), я вызываю функцию печати отладки, которая печатает только условно, поэтому такая же сборка код может печатать или не печатать в зависимости от параметров программы). Я защищаю то, что для всех сборок компилятор должен видеть диагностические утверждения; однако, он не будет генерировать код, если не включена отладка.
Джонатан Леффлер

8

Согласно http://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html , должно быть ##до __VA_ARGS__.

В противном случае, макрос #define dbg_print(format, ...) printf(format, __VA_ARGS__)не будет скомпилировать следующий пример: dbg_print("hello world");.


1
Добро пожаловать в стек переполнения. Вы правы, что GCC имеет нестандартное расширение, на которое вы ссылаетесь. Фактически принятый ответ действительно упоминает об этом, включая именно тот URL-адрес, который вы даете.
Джонатан Леффлер

7
#define debug_print(FMT, ARGS...) do { \
    if (DEBUG) \
        fprintf(stderr, "%s:%d " FMT "\n", __FUNCTION__, __LINE__, ## ARGS); \
    } while (0)

Какая версия C поддерживает эту запись? И, если это сработало, токен, вставляющий все подобные аргументы, означает, что у вас есть только очень ограниченный набор опций для строки формата, не так ли?
Джонатан Леффлер

@Jonathan: gcc (Debian 4.3.3-13) 4.3.3
eyalm

1
ОК - согласовано: оно задокументировано как старое расширение GNU (раздел 5.17 руководства GCC 4.4.1). Но вам, вероятно, следует задокументировать, что он будет работать только с GCC - или, возможно, мы сделали это между нами в этих комментариях.
Джонатан Леффлер

1
Я намеревался показать другой стиль использования аргументов и, главным образом, продемонстрировать использование FUNCTION и LINE
eyalm

2

Это то, что я использую:

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

Это имеет хорошее преимущество для правильной обработки printf даже без дополнительных аргументов. В случае DBG == 0 даже самому глупому компилятору нечего жевать, поэтому код не генерируется.


Лучше, чтобы компилятор всегда проверял код отладки.
Джонатан Леффлер

1

Мой любимый из приведенных ниже var_dump, который, когда называется как:

var_dump("%d", count);

производит вывод как:

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

Кредит @ "Джонатан Леффлер". Все счастливы C89:

Код

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

1

Итак, при использовании gcc мне нравится:

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

Потому что это может быть вставлено в код.

Предположим, вы пытаетесь отладить

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

720

Затем вы можете изменить его на:

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

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

И вы можете получить анализ того, какое выражение было оценено для чего.

Он защищен от проблемы двойной оценки, но отсутствие гензимов делает его открытым для коллизий имен.

Однако это гнездо:

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

Поэтому я думаю, что пока вы не будете использовать g2rE3 в качестве имени переменной, все будет в порядке.

Конечно, я нашел его (и смежные версии для строк, и версии для уровней отладки и т. Д.) Бесценными.


1

Я много лет думал о том, как это сделать, и наконец нашел решение. Однако я не знал, что здесь уже были другие решения. Во-первых, в отличие от ответа Леффлера , я не вижу его аргумента в пользу того, что отладочные отпечатки всегда должны компилироваться. Я бы предпочел не иметь тонны ненужного кода, выполняемого в моем проекте, когда он не нужен, в тех случаях, когда мне нужно тестировать, и они могут не оптимизироваться.

Не компиляция каждый раз может звучать хуже, чем на практике. Вы получаете отладочные отпечатки, которые иногда не компилируются, но их не так сложно скомпилировать и протестировать перед завершением проекта. В этой системе, если вы используете три уровня отладок, просто поместите их на уровень отладочных сообщений три, исправьте ошибки компиляции и проверьте все остальные, прежде чем завершить работу с кодом. (Поскольку, конечно, компиляция отладочных операторов не является гарантией того, что они все еще работают так, как задумано.)

Мое решение также предусматривает уровни детализации отладки; и если вы установите его на самый высокий уровень, они все скомпилируются. Если вы недавно использовали высокий уровень детализации отладки, все они могли компилироваться в то время. Окончательные обновления должны быть довольно легкими. Мне никогда не требовалось больше трех уровней, но Джонатан говорит, что он использовал девять. Этот метод (как и метод Леффлера) можно распространить на любое количество уровней. Использование моего метода может быть проще; требуется только два утверждения при использовании в вашем коде. Однако я тоже кодирую макрос CLOSE - хотя он ничего не делает. Возможно, если бы я отправлял в файл.

Против стоимости дополнительный шаг тестирования их, чтобы видеть, что они будут компилироваться перед поставкой, состоит в том, что

  1. Вы должны доверять им, чтобы их оптимизировали, что по общему признанию ДОЛЖНО произойти, если у вас есть достаточный уровень оптимизации.
  2. Кроме того, они, вероятно, не сработают, если вы сделаете компиляцию релиза с отключенной оптимизацией для целей тестирования (что, по общему признанию, редко); и они почти наверняка не будут вообще во время отладки - тем самым выполняя десятки или сотни операторов if (DEBUG) во время выполнения; таким образом, замедление выполнения (что является моим главным возражением) и, что менее важно, увеличение размера исполняемого файла или DLL; и, следовательно, время выполнения и компиляции. Джонатан, однако, сообщает мне, что его метод может быть сделан, чтобы вообще не компилировать утверждения.

Ветви на самом деле довольно дорогие в современных процессорах предварительной выборки. Возможно, нет ничего страшного, если ваше приложение не является критичным по времени; но если производительность - это проблема, то да, достаточно крупная сделка, которую я предпочел бы выбрать для более быстрого выполнения отладочного кода (и, возможно, более быстрого выпуска, в редких случаях, как отмечалось).

Итак, я хотел отладочный макрос печати, который не компилируется, если его не нужно печатать, но делает, если он есть. Я также хотел уровни отладки, чтобы, например, если бы я хотел, чтобы важные части кода не печатались в некоторых случаях, а печатались в других, я мог бы установить уровень отладки и получить дополнительные отладочные отпечатки. I наткнулся на способ реализации уровней отладки, которые определяли, был ли отпечаток даже скомпилирован или нет. Я достиг этого так:

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

Использование макросов

Чтобы использовать это, просто сделайте:

DEBUGLOG_INIT("afile.log");

Для записи в файл журнала просто выполните:

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

Чтобы закрыть его, вы делаете:

DEBUGLOG_CLOSE();

хотя в настоящее время это даже не нужно, технически говоря, поскольку это ничего не делает. Я все еще использую ЗАКРЫТЬ прямо сейчас, однако, на случай, если я передумаю о том, как это работает, и хочу оставить файл открытым между инструкциями регистрации.

Затем, когда вы хотите включить отладочную печать, просто отредактируйте первый #define в заголовочном файле, чтобы сказать, например,

#define DEBUG 1

Чтобы операторы регистрации ничего не компилировали, выполните

#define DEBUG 0

Если вам нужна информация из часто выполняемого фрагмента кода (т.е. с высоким уровнем детализации), вы можете написать:

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

Если вы определили, что DEBUG равен 3, уровни логирования 1, 2 и 3 компилируются. Если вы установите его на 2, вы получите уровни ведения журнала 1 и 2. Если вы установите его на 1, вы получите только операторы уровня регистрации 1.

Что касается цикла do-while, поскольку он оценивает либо одну функцию, либо ничего, вместо оператора if цикл не нужен. Хорошо, обвини меня в том, что я использую C вместо C ++ IO (и Qtring :: arg () в Qt - более безопасный способ форматирования переменных, когда в Qt тоже - это довольно удобно, но требует больше кода, а документация по форматированию не так организована как это может быть - но все же я нашел случаи, когда это предпочтительнее), но вы можете поместить любой код в файл .cpp, который вы хотите. Это также может быть класс, но тогда вам нужно будет создать его экземпляр и не отставать от него, или выполнить new () и сохранить его. Таким образом, вы просто добавляете операторы #include, init и, возможно, закрываете их в свой источник, и вы готовы начать их использовать. Однако, если бы вы были так склонны, это было бы хорошим уроком.

Ранее я видел много решений, но ни одно из них не соответствовало моим критериям так же, как это.

  1. Это может быть расширено, чтобы сделать столько уровней, сколько хотите.
  2. Он ничего не компилирует, если не печатает.
  3. Он централизует ввод-вывод в одном легко редактируемом месте.
  4. Это гибкий, используя форматирование printf.
  5. Опять же, это не замедляет отладочные запуски, тогда как всегда компилируемые отладочные отпечатки всегда выполняются в режиме отладки. Если вы занимаетесь информатикой, и вам не сложно писать обработку информации, вы можете запустить симулятор, потребляющий процессор, например, где отладчик останавливает его с индексом вне диапазона для вектора. Они работают очень медленно в режиме отладки уже. Обязательное выполнение сотен отладочных отпечатков обязательно замедлит такие прогоны еще больше. Для меня такие пробеги не редкость.

Не очень важно, но в дополнение:

  1. Не требует взлома для печати без аргументов (например DEBUGLOG_LOG(3, "got here!");); что позволяет вам использовать, например, более безопасное форматирование .arg () в Qt. Это работает на MSVC, и, таким образом, вероятно, GCC. Он использует ##в #defines, что является нестандартным, как указывает Леффлер, но широко поддерживается. (Вы можете перекодировать его, чтобы не использовать ##при необходимости, но вам придется использовать взлом, такой как он.)

Предупреждение. Если вы забудете предоставить аргумент уровня ведения журнала, MSVC бесполезно заявляет, что идентификатор не определен.

Возможно, вы захотите использовать имя символа препроцессора, отличное от DEBUG, так как некоторые источники также определяют этот символ (например, программы используют ./configureкоманды для подготовки к сборке). Мне казалось естественным, когда я его разработал. Я разработал его в приложении, где DLL используется чем-то другим, и более удобно отправлять распечатки журнала в файл; но изменение его на vprintf () тоже подойдет.

Я надеюсь, что это спасет многих из вас от горя при поиске лучшего способа ведения журнала отладки; или показывает вам тот, который вы можете предпочесть. Я нерешительно пытался понять это в течение десятилетий. Работает в MSVC 2012 и 2015, и, вероятно, на GCC; а также, вероятно, работает над многими другими, но я не проверял это на них.

Я хочу сделать потоковую версию этого дня тоже.

Примечание: спасибо Леффлеру, который помог мне лучше отформатировать мое сообщение для StackOverflow.


2
Вы говорите: «Выполнение десятков или сотен if (DEBUG)операторов во время выполнения, которые не оптимизируются», что является отклонением на ветряных мельницах . Весь смысл этой системы я описал, что код проверяется компилятором (важно, и автоматический - не требуется специальная сборка) , но код отладки не генерируются на всех , потому что это оптимизированное (так что нулевое воздействии выполнения на размер кода или производительность, потому что код не присутствует во время выполнения).
Джонатан Леффлер

Джонатан Леффлер: Спасибо за указание на мою неправильную формулировку. Я позволяю своим мыслям биться быстрее, чем мои пальцы, и я так рад, что все закончилось. Я пересмотрел свои возражения: «... 1) вы должны доверять им, чтобы их оптимизировали, что по общему признанию должно произойти, если у вас есть достаточный уровень оптимизации. 2) Более того, они не будут, если вы сделаете релиз, скомпилированный с оптимизацией отключен для целей тестирования, и они, вероятно, вообще не будут работать во время отладки - тем самым выполняя десятки или сотни операторов if (DEBUG) во время выполнения - тем самым увеличивая размер исполняемого файла или dll, а также время выполнения ».
CodeLurker

Чтобы вы могли сделать другую важную вещь, которую делает моя, вам понадобятся уровни отладки. Хотя мне часто не нужно, чтобы многие из них были включены, некоторые приложения действительно извлекают выгоду из возможности получить высокий уровень детализации о критичном по времени цикле с простым «#define DEBUG 3», а затем вернуться к гораздо меньше подробной информации с "#define DEBUG 1". Мне никогда не требовалось более трех уровней, и, таким образом, по крайней мере примерно треть моих отладок компилируется уже при выпуске. Если я недавно использовал уровень 3, они, вероятно, ВСЕ делают.
CodeLurker

YMMV. Современная система, которую я показал, поддерживает динамическую (во время выполнения) настройку уровней отладки, так что вы можете программно решить, какая часть отладки создается во время выполнения. Я обычно использовал уровни 1-9, хотя верхнего предела нет (или нижнего предела; уровень по умолчанию 0, который обычно отключен, но может быть явно запрошен во время активной разработки, если это уместно - он не подходит для длительной работы). Я выбрал уровень по умолчанию 3; вещи могут быть настроены. Это дает мне много контроля. Если вы действительно не хотите проверять код отладки, когда он неактивен, измените альтернативу на ((void)0)- это легко.
Джонатан Леффлер

1
Ааа. Это помогло бы прочитать все это. Это довольно длинный пост. Я думаю, что до сих пор есть важные моменты. Оказывается, ваш, как и мой, может использоваться для компиляции или не компиляции всех отладочных отпечатков и может поддерживать уровни; хотя по общему признанию, вы можете компилировать уровни, которые вы не используете - за плату во время отладки.
CodeLurker

0

Я считаю, что этот вариант темы дает отладочные категории без необходимости иметь отдельное имя макроса для каждой категории.

Я использовал этот вариант в проекте Arduino, где пространство программы ограничено 32 КБ, а динамическая память ограничена 2 КБ. Добавление операторов отладки и строк отладки трассировки быстро занимает место. Поэтому важно иметь возможность ограничить трассировку отладки, включенную во время компиляции, до минимума, необходимого при каждой сборке кода.

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

вызов файла .cpp

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

...
PRINT(4, "Time out error,\t");
...
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.