安全なデバッグマクロ定義に関する考察
Tagged:

例えば、 C/C++ で開発を行っている現場であれば、 以下のようなマクロが1つや2つや3つや4つや N 個は有るのではないでしょうか。

#ifdef DEBUG

extern int debug_level;

/* 閾値以下のレベル値の場合に、式 exp を評価するマクロ */
#define AT_DEBUG(level, exp) \
    if(level <= debug_level){ exp; }

#else

/* DEBUG マクロが定義されていない場合はコードを丸ごと除外 */
#define AT_DEBUG(level, exp) /* nop */

#endif    
AT_DEBUG() の定義 (1)

このマクロは、以下のようにして使用します。

    value = get_value();
    AT_DEBUG(1, printf("value=%s", value));
AT_DEBUG() の使用

人によっては、 AT_DEBUG() マクロを以下のように定義する人がいるかもしれません。

#define AT_DEBUG(level, exp) \
    if(level <= debug_level){ exp; }else
AT_DEBUG() の定義 (2)

定義 (2) のような書き方をする人は、 if/else 記述にブロックステートメントを使用しない、 古くからのカーネルソースやフリーソフトウェアでの実装機会が、 多かったのではないでしょうか。

例えば、以下のソースを対象として考えて見ましょう。

    if(condA)
        AT_DEBUG(1, printf("condA is true"));
    else
        statementB;
元ソース

定義 (1) の AT_DEBUG() の場合、 上記のソースはプリプロセス後に以下のように展開されます。

    if(condA)
        if(1 <= debug_level){ printf("condA is true"); };
    else
        statementB;
マクロ展開イメージ(1)

以上のソースを(狭義の) C コンパイラが解釈した場合、 if(condA) から始まる if ステートメントは、 元ソースの AT_DEBUG() 末尾に書かれたセミコロンによって終了させられてしまいます。 結果として、それに続く else 節は「if ステートメントを持たない」 不正な記述とみなされるか、 「さらに上位の if ステートメントの一部」とみなされますので、 いずれにしても誤って解釈されることになります。

かと言って、 「このような場合にのみ AT_DEBUG() 末尾のセミコロンを省略」したのでは、 記述が統一されません (ついでに Emacs による整形等が上手く機能しなくなります)。

つまり、 定義 (1) の AT_DEBUG() は、 if/else での記述にブロックステートメントを使用する今時の記述では、 一見問題の無いように見えますが、 旧形式の実装との混在を考えた場合、 必ずしも妥当ではないと言えます

ここで定義 (2) の AT_DEBUG() を用いた場合:

    if(condA)
        if(1 <= debug_level){ printf("condA is true"); }else;
    else
        statementB;
マクロ展開イメージ(2)

AT_DEBUG() 末尾のセミコロンは「直前の else 節を終了させるもの」とみなされるため、 if(condA) から始まる if ステートメントは、 元ソースにおける else 節まで継続します。 これにより、 期待するコンパイル結果を得ることができます。

このような状況では有効な定義 (2) の AT_DEBUG() ですが、 以下のような場合には、 原因を特定し難い障害の原因と成り得る問題を抱えています。

    statementA;
    AT_DEBUG(1, printf("statementA is passed"))
    statementB;
else の副作用

上記のソースで注意して欲しいのは、 AT_DEBUG() 末尾のセミコロンを忘れている、 という、 非常にありふれて且つ気付きにくい間違いを犯している点です。

定義 (1) の AT_DEBUG 定義は、 元々末尾がブロックステートメントなので、 実装した際の意図に沿った解釈がなされます (「セミコロンを書き忘れていてもコンパイルが通ってしまう」というのは、 別な意味で問題ではあるのですが…)。

その一方で、 定義 (2) の AT_DEBUG() の場合、 このソースがプリプロセッサによって展開されると以下のようになります (こちらもコンパイルは通ります)。

    statementA;
    if(1 <= debug_level){ printf("statementA is passed"); }else
    statementB;
マクロ展開イメージ(3)

マクロ展開後のソースでは、 statementB が AT_DEBUG() 末尾の else 節の一部とみなされるため、 実装者が意図したものと全く異なる解釈がなされてしまいます。

歴史的諸々の経緯(記述量削減等)があったのでしょうが、 どこかの時点で、 「if/else 等の制御構文にはブロックステートメント以外記述できない」 ぐらいの大鉈を振るっても良かったのではないかと思います (C 言語の普及度・重要度がそれをさせなかったのでしょうね)。

話題を AT_DEBUG() に戻しますが、 結局のところ、 妥当な定義は以下のような感じでしょうか。

#define AT_DEBUG(level, exp) \
    ((debug_level < (level)) || ((exp), 0))
安全な AT_DEBUG() 定義

このように定義することで、 AT_DEBUG() 自体は「式」と等価になりますから、 前述したような記述位置に関わる問題は発生しません。 また、セミコロンの記述忘れに際しても、 常にコンパイルエラーが検出されます。

"(exp, 0)" 記述は、 exp が戻り値 void の関数だった場合に、 上記マクロを「式」として成立させる(= 値を持たせる) ためのものです。 これは「コンマ演算」(by K&R)と呼ばれるもので、 相当 C 言語を使い込んでいる人でないと知らない可能性が高いですが、 ユーティリティマクロ定義等では結構重宝しますので、 覚えておいて損はありません。

なお、上記のマクロ定義は 「条件評価は左から」という前提を元に書かれているため、 「|| や && で結合された式の評価順序はコンパイラの実装依存」 という C 言語の仕様から見たら、 建前上は安全性の面で完全ではありません。

所謂 "else" に当たる部分の記述が 「気分的に」冗長だったので採用しなかったのですが、 言語仕様上から見ても安全な側に振っておきたいのであれば、 以下のような記述が良いでしょう。

#define AT_DEBUG(level, exp) \
    (((level) <= debug_level) ? ((exp), 0) : 0)
言語仕様上の点でも安全な AT_DEBUG() 定義