1. ホーム
  2. c

[解決済み】C言語でデバッグ印刷を行うための#defineマクロ?

2022-04-03 14:31:58

質問

DEBUGが定義されているときに、以下の疑似コードのようなデバッグメッセージを表示するマクロを作成しようとしています。

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

マクロでどのように実現するのですか?

どのように解決するのですか?

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を使ったところをDEBUG_TESTにします。

フォーマット文字列の文字列リテラルにこだわるなら(おそらくいずれにせよ良いアイデアです)、次のようなものも導入できます。 __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マクロの評価値が0以外の場合にのみ印刷関数を呼び出します。

このため、'stderr' のようなものを処理するためのサポート関数 - 例では dbg_printf() - が必要です。 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のようなステートメントを見るということです。 これは、10年、20年と続くような長期的なコードにとって重要なことです。

あるコードが何年もの間、ほとんど休眠状態(安定状態)であったが、この度、変更が必要になったとします。 しかし、デバッグ(トレース)コードが、何年もの安定したメンテナンスの間に、名前を変更したり、タイプを変更したりした変数を参照しているため、デバッグしなければならないのは、いらいらすることです。コンパイラ(ポストプリプロセッサ)が常にprint文を見ていれば、周囲の変更が診断結果を無効にしていないことを確認することができます。もしコンパイラがprint文を見なければ、あなた自身の不注意(あるいはあなたの同僚や共同研究者の不注意)からあなたを守ることはできないのです。参照 ' プログラミングの実践 カーニガンとパイク著、特に第8章(Wikipediaの TPOP ).

これは「そこにいて、それをやった」経験です。私は何年もの間(10年以上)、非デバッグビルドで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 以降の単一引数バリアント

Kyle Brandtさんからの質問です。

どうにかして、このように 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() はコンパイルに失敗します)。 チェック機能が失われることがまったく問題でないかどうかは議論の余地がある。

引数が1つの場合のGCC特有のテクニック

コンパイラによっては、マクロで可変長の引数リストを処理する他の方法のための拡張機能を提供する場合があります。 具体的には、最初にコメントで述べたように ユーゴ・アイデラー GCCでは、マクロの最後の「固定」引数の後に通常表示されるカンマを省略することができます。 また ##__VA_ARGS__ は、マクロの置換テキストで、前のトークンがカンマである場合にのみ、記法の前のカンマを削除します。

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

この解決策では、format 引数を必要とする利点はそのままに、format の後にオプションの引数を受け付けます。

このテクニックは 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の私の ソーク (スタック オーバーフローの質問)レポジトリをファイルとして debug.c , debug.hmddebug.c において src/libsoq サブディレクトリに格納されます。