#definir macro para depurar la impresión en C?

Resuelto jfarrell asked hace 14 años • 14 respuestas

Intentando crear una macro que pueda usarse para imprimir mensajes de depuración cuando se define DEBUG, como el siguiente pseudocódigo:

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

¿Cómo se logra esto con una macro?

jfarrell avatar Oct 29 '09 23:10 jfarrell
Aceptado

Si utiliza un compilador C99 o posterior

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

Se supone que está utilizando C99 (la notación de lista de argumentos variables no es compatible con versiones anteriores). El do { ... } while (0)modismo garantiza que el código actúe como una declaración (llamada a función). El uso incondicional del código garantiza que el compilador siempre verifique que su código de depuración sea válido, pero el optimizador eliminará el código cuando DEBUG sea 0.

Si desea trabajar con #ifdef DEBUG, cambie la condición de prueba:

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

Y luego use DEBUG_TEST donde usé DEBUG.

Si insiste en un literal de cadena para la cadena de formato (probablemente sea una buena idea de todos modos), también puede introducir cosas como __FILE__y __LINE__en __func__la salida, lo que puede mejorar el diagnóstico:

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

Esto se basa en la concatenación de cadenas para crear una cadena de formato más grande que el que escribe el programador.

Si usa un compilador C89

Si está atascado con C89 y no tiene una extensión de compilación útil, entonces no existe una forma particularmente limpia de manejarlo. La técnica que solía utilizar era:

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

Y luego, en el código, escribe:

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

Los paréntesis dobles son cruciales y es por eso que tienes la notación divertida en la expansión macro. Como antes, el compilador siempre verifica la validez sintáctica del código (lo cual es bueno), pero el optimizador solo invoca la función de impresión si la macro DEBUG se evalúa como distinta de cero.

Esto requiere una función de soporte (dbg_printf() en el ejemplo) para manejar cosas como 'stderr'. Requiere que sepas escribir funciones varargs, pero eso no es difícil:

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

También puedes usar esta técnica en C99, por supuesto, pero la __VA_ARGS__técnica es más clara porque usa notación de función normal, no el truco de doble paréntesis.

¿Por qué es fundamental que el compilador siempre vea el código de depuración?

[ Comentarios repetidos hechos a otra respuesta. ]

Una idea central detrás de las implementaciones C99 y C89 anteriores es que el compilador propiamente dicho siempre ve las declaraciones de depuración tipo printf. Esto es importante para el código a largo plazo: código que durará una o dos décadas.

Supongamos que un fragmento de código ha estado prácticamente inactivo (estable) durante varios años, pero ahora es necesario cambiarlo. Vuelve a habilitar el seguimiento de depuración, pero es frustrante tener que depurar el código de depuración (rastreo) porque se refiere a variables que han sido renombradas o reescribidas durante los años de mantenimiento estable. Si el compilador (postpreprocesador) siempre ve la declaración de impresión, se asegura de que cualquier cambio circundante no haya invalidado el diagnóstico. Si el compilador no ve la declaración impresa, no puede protegerlo contra su propio descuido (o el descuido de sus colegas o colaboradores). Véase ' La práctica de la programación ' de Kernighan y Pike, especialmente el Capítulo 8 (véase también Wikipedia sobre TPOP ).

Esta es la experiencia de "he estado allí, he hecho eso": utilicé esencialmente la técnica descrita en otras respuestas donde la compilación sin depuración no ve las declaraciones tipo printf durante varios años (más de una década). Pero encontré el consejo en TPOP (ver mi comentario anterior), y luego habilité parte del código de depuración después de varios años, y me encontré con problemas de cambio de contexto que interrumpieron la depuración. Varias veces tener la impresión siempre validada me ha salvado de problemas posteriores.

Utilizo NDEBUG solo para controlar aserciones y una macro separada (generalmente DEBUG) para controlar si el seguimiento de depuración está integrado en el programa. Incluso cuando el seguimiento de depuración está integrado, con frecuencia no quiero que la salida de depuración aparezca incondicionalmente, por lo que tengo un mecanismo para controlar si aparece la salida (niveles de depuración, y en lugar de llamar fprintf()directamente, llamo a una función de impresión de depuración que solo imprime condicionalmente para que la misma compilación del código pueda imprimirse o no según las opciones del programa). También tengo una versión del código de 'subsistemas múltiples' para programas más grandes, de modo que puedo tener diferentes secciones del programa produciendo diferentes cantidades de seguimiento, bajo control de tiempo de ejecución.

Estoy defendiendo que para todas las compilaciones, el compilador debería ver las declaraciones de diagnóstico; sin embargo, el compilador no generará ningún código para las declaraciones de seguimiento de depuración a menos que la depuración esté habilitada. Básicamente, significa que el compilador verifica todo su código cada vez que lo compila, ya sea para su lanzamiento o depuración. ¡Ésto es una cosa buena!

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

Variante de argumento único para C99 o posterior

Kyle Brandt preguntó:

De todos modos, ¿hacer esto debug_printfunciona incluso si no hay argumentos? Por ejemplo:

    debug_print("Foo");

Hay un truco simple y anticuado:

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

La solución exclusiva de GCC que se muestra a continuación también brinda soporte para eso.

Sin embargo, puedes hacerlo con el sistema C99 directo usando:

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

En comparación con la primera versión, se pierde la verificación limitada que requiere el argumento 'fmt', lo que significa que alguien podría intentar llamar a 'debug_print()' sin argumentos (pero la coma final en la lista de argumentos no se fprintf()compilaría). . Es discutible si la pérdida de la cuenta corriente es un problema.

Técnica específica de GCC para un solo argumento

Algunos compiladores pueden ofrecer extensiones para otras formas de manejar listas de argumentos de longitud variable en macros. Específicamente, como se señaló por primera vez en los comentarios de Hugo Ideler , GCC le permite omitir la coma que normalmente aparecería después del último argumento "fijo" de la macro. También le permite usar ##__VA_ARGS__en la macro texto de reemplazo, que elimina la coma que precede a la notación si, pero solo si, el token anterior es una coma:

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

Esta solución conserva la ventaja de requerir el argumento de formato y al mismo tiempo aceptar argumentos opcionales después del formato.

Clang también admite esta técnica para compatibilidad con GCC.


¿Por qué el ciclo do- while?

¿Cuál es el propósito del do whileaquí?

Desea poder utilizar la macro para que parezca una llamada de función, lo que significa que irá seguida de un punto y coma. Por lo tanto, debe empaquetar el cuerpo de la macro para que se adapte a sus necesidades. Si usa una ifdeclaración sin el entorno do { ... } while (0), tendrá:

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

Ahora supongamos que escribe:

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

Desafortunadamente, esa sangría no refleja el control real del flujo, porque el preprocesador produce un código equivalente a este (sangrado y llaves agregadas para enfatizar el significado real):

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

El próximo intento de macro podría ser:

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

Y el mismo fragmento de código ahora produce:

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

Y elseahora hay un error de sintaxis. El do { ... } while(0)bucle evita ambos problemas.

Hay otra forma de escribir la macro que podría funcionar:

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

Esto deja el fragmento del programa mostrado como válido. La (void)conversión evita que se use en contextos donde se requiere un valor, pero podría usarse como operando izquierdo de un operador de coma donde la do { ... } while (0)versión no puede. Si cree que debería poder incrustar código de depuración en dichas expresiones, quizás prefiera esto. Si prefiere exigir que la impresión de depuración actúe como una declaración completa, entonces la do { ... } while (0)versión es mejor. Tenga en cuenta que si el cuerpo de la macro incluye punto y coma (en términos generales), solo puede utilizar la do { ... } while(0)notación. Siempre funciona; el mecanismo de declaración de expresión puede ser más difícil de aplicar. También puede recibir advertencias del compilador con la forma de expresión que preferiría evitar; Dependerá del compilador y de los indicadores que utilices.


TPOP estaba anteriormente en http://plan9.bell-labs.com/cm/cs/tpop y http://cm.bell-labs.com/cm/cs/tpop pero ahora ambos están (2015-08-10) roto.


Código en GitHub

Si tiene curiosidad, puede ver este código en GitHub en mi repositorio SOQ (Preguntas de desbordamiento de pila) como archivos debug.cy en el subdirectorio src/libsoq .debug.hmddebug.c

Jonathan Leffler avatar Oct 29 '2009 16:10 Jonathan Leffler

Yo uso algo como esto:

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

Entonces solo uso D como prefijo:

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

El compilador ve el código de depuración, no hay problema de coma y funciona en todas partes. También funciona cuando printfno es suficiente, por ejemplo cuando se debe volcar una matriz o calcular algún valor de diagnóstico que sea redundante para el programa mismo.

EDITAR: Ok, podría generar un problema cuando hay elsealgún lugar cercano que pueda ser interceptado por este archivo inyectado if. Esta es una versión que lo repasa:

#ifdef DEBUG
 #define D 
#else
 #define D for(;0;)
#endif
mbq avatar Sep 18 '2011 20:09 mbq