C言語の__LINE__マクロをプリプロセッサの段階で文字列に変換する

__LINE__マクロをプリプロセッサの段階で文字列に変換する方法について記載します。

 

1 __LINE__マクロ

プリプロセッサが__LINE__マクロを行番号の整数値に変換します。

以下のコードは行番号の整数を"%d"のフォーマットで文字列に変換します。

#include <stdio.h>

int main(void)
{
  printf("%d\n", __LINE__);
  return 0;
}

実行結果は以下の通りです。

5

これはプログラム実行時に"%d"を走査して行番号の整数値に置き換える動作が必要になります。

2 #演算子

プリプロセッサが#<identifier>の<identifier>を文字列に変換します。

以下のプログラムはプリプロセッサの段階で識別子aとbを#演算子で文字列に変換しつつ、プログラム実行時に"%d"で文字列に変換します。

#include <stdio.h>

#define print(value) printf(#value " = %d\n", value)

int main(void)
{
  int a = 1, b = 2;
  print(a);
  print(b);
  return 0;
}

実行結果は以下の通りです。

a = 1
b = 2

3 __LINE__と#演算子

以下のSTRINGIFYマクロとTOSTRINGマクロを定義します。

TOSTRINGマクロはSTRINGIFYマクロを置換しただけのものに見えますが、入れ子になったマクロである為、展開結果は異なります。

#include <stdio.h>

#define STRINGIFY(n) #n
#define TOSTRING(n) STRINGIFY(n)

int main(void)
{
  puts(STRINGIFY(__LINE__));
  puts(TOSTRING(__LINE__));
  return 0;
}

実行結果は以下のようになります。

__LINE__
9

STRINGIFY(__LINE__)を直接呼び出した場合は"__LINE__"になります。

__LINE__が行番号に変換されるよりも#__LINE__の文字列変換の方が早く実行されます。

STRINGIFY(__LINE__) -> #__LINE__ -> "__LINE__"

TOSTRING(__LINE__)がSTRINGIFYに変換されると同時に__LINE__が行番号の整数値に変換されます。

結果STRINGIFY(9)が呼び出されることになります。

TOSTRING(__LINE__) -> STRINGIFY(9) -> #9 -> "9"

プリプロセッサは入れ子になったマクロを段階的に展開していきます。

この性質を用いて、__LINE__で取得できる行番号の整数値をプリプロセッサの段階で文字列に変換することができます。

 

メリットはプログラム実行時に"%d"を用いた文字列変換が必要ではなくなる点です。

デメリットは文字列に変換された行番号の分だけ文字列リテラルの領域が増える点です。

4 サンプルコード

以下は__LINE__マクロをプリプロセッサの段階で文字列に変換し、ログとして用いるサンプルコードです。

#include <stdio.h>

#define STRINGIFY(n) #n
#define TOSTRING(n) STRINGIFY(n)
#define PREFIX __FILE__ ":" TOSTRING(__LINE__) ": "

#define log(fp, prefix, ...)                    \
  do {                                          \
    fprintf(fp, prefix " " PREFIX __VA_ARGS__); \
    fprintf(fp, "\n");                          \
  } while (0)

#ifdef DEBUG
#define debug(args...) log(stdout, "[d]", args)
#else
#define debug(...) /* Do nothing. */
#endif

#define info(args...) log(stdout, "[i]", args)
#define error(args...) log(stderr, "[e]", args)

int main(void)
{
  info("running");
  debug("hello, world");
  debug("%s", "hello, world");
  return 0;
}

CFLAGSに-DDEBUGを付けた状態でコンパイルします。実行結果は以下の通りです。

[i] a.c:24: running
[d] a.c:25: hello, world
[d] a.c:26: hello, world

"<ファイル名>:<行数>:"のログ形式にしておくと、emacsのバッファ上で実行した場合に、RETキーで該当箇所にジャンプすることができます。