__VA_ARGS__の引数をループするやつはなぜ動くのか

今更マクロの話です。求められてるのはそういうのではなくモダンな解決方法の話だということは知っているけれど、例えばreflection TSとかは紹介するには重すぎるので……。まあとりあえず今回は伝統的なテクニックの話をします。他所でも解説されている可能性は高いけれど、同じ解説というものは二つは生まれないものだろうから気にせず書こう。

可変引数マクロ

C99(C++11)から可変引数マクロというのが入った。簡単にいうと任意個の引数を取れるマクロだ。#define MACRO(x, y, ...)のように定義する。

__VA_ARGS__というのはこの可変引数マクロで使うもので、展開した時の...の中身が入っている。これを使うと、以下のようなマクロが書ける。

#define DEBUG_PRINT(fmt, ...) \
    printf("at line %d: " fmt, __LINE__, __VA_ARGS__);

DEBUG_PRINT("%9.4f %9.4f %9.4f\n", x, y, z);
// => printf("at line %d: " "%9.4f %9.4f %9.4f\n", x, y, z);

問題意識

さて、しかしこれはもう少し便利にできそうだ。例えば、これくらい簡単になってくれると助かることもある。

DEBUG_PRINT(x, y, z)
// => std::cerr << "at line " << __LINE__ << ": "
//              << " x = " << x
//              << " y = " << y
//              << " z = " << z
//              << std::endl;

これを実装するには、__VA_ARGS__の要素ごとにループしなければならない。これはちょっと難しい。プリプロセッサマクロでは、展開中にすでに展開されたマクロは展開されない。なので、

#define RECURSION(x) std::cout << x << std::endl ; RECURSION(x)
RECURSION("Don't stop me now!");

のようなマクロは一回展開されただけで終わってしまう(最初のRECURSIONを展開している時に出てくるRECURSIONは、今まさに展開したマクロなので「展開禁止リスト」に入っているため二回目の展開が行われない)。

これが別のマクロなら構わないわけなので、例えば以下のような展開は可能だ。

#define RECURSION_1(x) std::cout << x << std::endl ;
#define RECURSION_2(x) std::cout << x << std::endl ; RECURSION_1(x)
#define RECURSION_3(x) std::cout << x << std::endl ; RECURSION_2(x)
#define RECURSION_4(x) std::cout << x << std::endl ; RECURSION_3(x)

RECURSION_4("Don't stop me now!") // => 4回展開される

なので、限られた個数なら以下のようにして DEBUG_PRINT を実装することができる。

#define STRINGIZE_AUX(x) #x
#define STRINGIZE(x) STRINGIZE_AUX(x)

#define RECURSION_1(x)      << " " STRINGIZE(x) " = " << x
#define RECURSION_2(x, ...) << " " STRINGIZE(x) " = " << x RECURSION_1(__VA_ARGS__)
#define RECURSION_3(x, ...) << " " STRINGIZE(x) " = " << x RECURSION_2(__VA_ARGS__)
#define RECURSION_4(x, ...) << " " STRINGIZE(x) " = " << x RECURSION_3(__VA_ARGS__)

#define DEBUG_PRINT_4(x, y, z, w) \
    std::cout << "line at " << __LINE__ << ": " \
        RECURSION_3(x, y, z, w) \
        << std::endl;

とはいえこれは厳しい。使うときに引数の個数によって名前を変えて呼ばないといけないとなると使いにくい。

なので、__VA_ARGS__の個数を数えられるようにすれば言いわけだ。普通ならループや再帰でやればいいのだが、マクロなのでそうはいかない。

__VA_ARGS__の個数を数える

ここである程度知られたテクニックがある。まず、逆順の数列(に展開されるマクロ)を用意する。

#define INDEX_RSEQ() 4, 3, 2, 1, 0

そして、それの上限個の引数を受け取って、上限+1個めの引数を返すマクロを定義する。

#define VA_ARGS_SIZE_IMPL(ARG1, ARG2, ARG3, ARG4, N, ...) N

使いやすいように少し整えると、

#define VA_ARGS_SIZE_AUX(...) \
    VA_ARGS_SIZE_IMPL(__VA_ARGS__)
#define VA_ARGS_SIZE(...) \
    VA_ARGS_SIZE_AUX(__VA_ARGS__, INDEX_RSEQ())

以下の形で個数を受け取れるようになる。

VA_ARGS_SIZE(__VA_ARGS__)

VA_ARGS_SIZE_IMPLがどうやって動いているのか少し見てみよう。VA_ARGS_SIZEに引数が上限の四つ渡されたとする。展開を遅延させるために挟んであるAUXは単に転送するだけなので飛ばす。するとVA_ARGS_SIZE_IMPLには以下の引数が渡される。

VA_ARGS_SIZE(x, y, z, w)
=> VA_ARGS_SIZE_IMPL(x, y, z, w, 4, 3, 2, 1, 0)

SIZE_IMPLは以下のような定義だった。

#define VA_ARGS_SIZE_IMPL(ARG1, ARG2, ARG3, ARG4, N, ...) N
//                           x,    y,    z,    w, 4, 3, 2, 1, 0

これの最初の4つがx, y, z, wによって消費されるので、展開結果は4になる。これは引数の個数と一致している。

3つになるとどうだろう。

#define VA_ARGS_SIZE_IMPL(ARG1, ARG2, ARG3, ARG4, N, ...) N
//                           x,    y,    z,    4, 3, 2, 1, 0

展開結果が3になった。この流れで、引数の個数が1個までならこれは機能する。0個の時は、...に相当する引数がないので厳密には正しくなくなる。ちなみに引数が一つも定義されていなければ引数はなくても問題ない。参照: 可変引数マクロ - cpprefjp C++日本語リファレンス

適切な深度のマクロを選ぶ

さて、ここまでで__VA_ARGS__に入っているものの個数を数えることはできるようになった。あとはこの数字を使って適切な再帰深度のマクロを呼べるようにすればいい。どうやって?

マクロを使えば、トークンの結合ができる。

#define CONCATENATE_AUX(x, y) x##y
#define CONCATENATE(x, y) CONCATENATE_AUX(x, y)

#define DEBUG_PRINT(...) \
    std::cout << "line at " << __LINE__ << ": " \
    CONCATENATE(RECURSION_, VA_ARGS_SIZE(__VA_ARGS__)) (__VA_ARGS__) \
    << std::endl;

RECURSION_VA_ARGS_SIZE(__VA_ARGS__)を結合すれば、RECURSION_と例えば3を結合することができ、適切な再帰深度のトークンを作ることができる。

ここでCONCATENATEの展開を遅延させておかないと、RECURSION_VA_ARGS_SIZE(...)のように展開されてしまうので注意。そのために_AUXなマクロが一枚噛まされている。

さて、これであとは、ここまで簡単のために4になっていた上限を十分大きいキリのいい値、例えば32とか64とか、心配なら256とかにしておけばDEBUG_PRINTが作れる。

もう一捻り

というわけで当初の目的は達成できているが、この方針では似たようなマクロを書く時にほぼ全てを書き直さなければならない。再帰の部分に労力がかかることを考えるとこれはかなり手間だ。できれば、ループ内の処理をマクロとして渡す、という形にしたい。

なのでまず、RECURSIONの部分をマクロを受け取るように書き換える。

#define FOR_EACH_VA_ARGS_AUX_1( FUNCTOR, ARG1     )\
    FUNCTOR(ARG1)
#define FOR_EACH_VA_ARGS_AUX_2( FUNCTOR, ARG1, ...)\
    FUNCTOR(ARG1) FOR_EACH_VA_ARGS_AUX_1( FUNCTOR, __VA_ARGS__)
// ...

そして形を整える。

#define FOR_EACH_VA_ARGS(FUNCTOR, ...) \
    CONCATENATE(FOR_EACH_VA_ARGS_AUX_, VA_ARGS_SIZE(__VA_ARGS__))(FUNCTOR, __VA_ARGS__)

こうしておくと、ループ部分を使いまわせるようになり、DEBUG_PRINTの実装は以下のようになる。

#define DEBUG_PRINT_AUX(x) \
    << " " STRINGIZE(x) " = " << x

#define DEBUG_PRINT(...) \
    std::cout << "at line " << __LINE__ << ": "\
        FOR_EACH_VA_ARGS(DEBUG_PRINT_AUX, __VA_ARGS__)\
        << std::endl

まとめ

__VA_ARGS__の引数をループするやつはこう書ける(上限付き)。実際に使う時には、マクロが衝突しないようにプロジェクト名とかのハンガリアンをつけておいた方が良い。

#define STRINGIZE_AUX(x) #x
#define STRINGIZE(x)     STRINGIZE_AUX(x)

#define CONCATENATE_AUX(x, y) x##y
#define CONCATENATE(x, y)     CONCATENATE_AUX(x, y)

#define INDEX_RSEQ() \
    32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, \
    16, 15, 14, 13, 12, 11, 10,  9,  8,  7,  6,  5,  4,  3,  2,  1, 0
#define VA_ARGS_SIZE_IMPL(\
        ARG1,  ARG2,  ARG3,  ARG4,  ARG5,  ARG6,  ARG7,  ARG8,  ARG9,  ARG10, \
        ARG11, ARG12, ARG13, ARG14, ARG15, ARG16, ARG17, ARG18, ARG19, ARG20, \
        ARG21, ARG22, ARG23, ARG24, ARG25, ARG26, ARG27, ARG28, ARG29, ARG30, \
        ARG31, ARG32, N, ...) N
#define VA_ARGS_SIZE_AUX(...) VA_ARGS_SIZE_IMPL(__VA_ARGS__)
#define VA_ARGS_SIZE(...)     VA_ARGS_SIZE_AUX(__VA_ARGS__, INDEX_RSEQ())

#define FOR_EACH_VA_ARGS_AUX_1( FUNCTOR, ARG1     ) FUNCTOR(ARG1)
#define FOR_EACH_VA_ARGS_AUX_2( FUNCTOR, ARG1, ...) FUNCTOR(ARG1) FOR_EACH_VA_ARGS_AUX_1( FUNCTOR, __VA_ARGS__)
// ... 中略 ...
#define FOR_EACH_VA_ARGS_AUX_32(FUNCTOR, ARG1, ...) FUNCTOR(ARG1) FOR_EACH_VA_ARGS_AUX_31(FUNCTOR, __VA_ARGS__)

#define FOR_EACH_VA_ARGS(FUNCTOR, ...) \
    CONCATENATE(FOR_EACH_VA_ARGS_AUX_, VA_ARGS_SIZE(__VA_ARGS__))(FUNCTOR, __VA_ARGS__)

使うときは以下のようにする。

#define DEBUG_PRINT_AUX(x) \
    << ", " STRINGIZE(x) " = " << x

#define DEBUG_PRINT(...) \
    std::cout << "at line " << __LINE__\
        FOR_EACH_VA_ARGS(DEBUG_PRINT_AUX, __VA_ARGS__)\
        << std::endl