[C++]可変引数マクロの引数を変換しつつ展開する方法を探る。

たぶん誰の役にも立たない記事。最近ブログを更新するネタがないので、気の抜けた記事の一つ二つ書いてもまあ良いかなぁと。

動機

私は自作ライブラリの中で、よく次のようなコードを書く。

auto [abc, def, ghi] = x.GetPlaceholders("abc"_fld, "def"_fld, "ghi"_fld);
auto defghi = def + ghi;
auto res = x | Filter(abc > 0) | Extract(abc.named("abc"), defghi.named("defghi"));

このコードがどういう意味かは今回の主題ではないし誰も興味を持たないと思うので省略する。ともかく、上のコードではabcdefといった変数名とそれの文字列での名前"abc""def"などとが全く同一で、とても冗長に見える。 今回はたかだか2~3個しか引数がないのでそれほど困らないのだが、このような冗長な名前が10個20個と連なることもあり、鬱陶しくて仕方がなかった。

このコードをマクロを使って、次のように簡略化できないだろうか。

GET_PLACEHOLDERS(x, abc, def, ghi);
auto defghi = def + ghi;
auto res = x | Filter(abc > 0) | Extract(NAMED_FLDS(abc, defghi));

ただし、GetPlaceholdersやExtractといった関数は任意個数の変数に対応しているので、マクロも同様に任意個数に対応しなければならない。かつ、""_fld.named("")のようなちょっと面倒な加工を一つ一つに施す必要がある。

私はマクロを極力使わないようにしてきたので、実のところそれほど深く理解していない。またMSVCの場合は可変引数マクロで特殊な挙動をするので、Clang/GCCのように直感的に記述できない。手探りで取り組んでみることにする。

実装

まずは単純に、与えられた引数に""_fldというユーザー定義リテラルを付与しつつ展開するマクロを考えてみる。

{ EXPAND_CONV(abc, def) };
//これは以下のよう展開されると期待される。
{ "abc"_fld, "def"_fld };

ではちょっと考えてみよう。

#define EXPAND_VARS(MACRO, ARGS) MACRO ARGS

#define EXPAND_CONV_1(CONV, x) CONV(x)
#define EXPAND_CONV_2(CONV, x, ...) CONV(x), EXPAND_VARS(EXPAND_CONV_1, (CONV, __VA_ARGS__))
#define EXPAND_CONV_3(CONV, x, ...) CONV(x), EXPAND_VARS(EXPAND_CONV_2, (CONV, __VA_ARGS__))
#define EXPAND_CONV_4(CONV, x, ...) CONV(x), EXPAND_VARS(EXPAND_CONV_3, (CONV, __VA_ARGS__))
#define EXPAND_CONV_5(CONV, x, ...) CONV(x), EXPAND_VARS(EXPAND_CONV_4, (CONV, __VA_ARGS__))

//上記はMSVCの可変引数マクロのバグを回避するためにEXPAND_VARSを挟み込んでおり、
//Clang/GCCだとそのままでは動かないので、以下のようにする。
//統一的な良い書き方がありそうな気はするが、ちょっと分からなかった。
//#define EXPAND_CONV_2(CONV, x, ...) CONV(x), EXPAND_CONV_1(CONV, __VA_ARGS__)

//ユーザー定義リテラルを付与
#define ADD_USER_LIT(x) #x##_fld

{ EXPAND_CONV_3(ADD_USER_LIT, x, y, z) };
//これは以下のように展開される。
{ "x"_fld, "y"_fld, "z"_fld }

とりあえず、最大5個の引数に対応する展開マクロを作成した。マクロの再帰はエラーになるので、長たらしいが多数定義するしかないように思われる。
CONVは各引数に対する変換のルールを受け取る引数で、上ではADD_USER_LITが与えられている。ここに例えば#define ADD_NAMED x.named(#x)などを定義して与えれば、x.named("x")のような展開に切り替わる。

EXPAND_CONV_Nはそれぞれ名前が異なるが、実際に使用するときに引数個数に応じてマクロを呼び分けるなどという意味不明なことをするわけには行かない。可変引数マクロから、引数の数に応じて呼び分けるように調整する。

可変引数マクロの引数の数を取得するには、次のようなトリックが使われる。よくこんなもの思いつくなぁ。

#define NUM_ARGS_2(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, N, ...) N
#define NUM_ARGS_1(...) EXPAND_VARS(NUM_ARGS_2, (__VA_ARGS__))
#define GET_NUM_ARGS(...) NUM_ARGS_1(__VA_ARGS__, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)

これらを組み合わせることで、先のGET_PLACEHOLDERなどのマクロを作成できるようになる。

#define CONCAT_(X,Y) X##Y
#define CONCAT(X,Y) CONCAT_(X,Y)
#define SELECT_MACRO_NUM(MACRO, CONV, ...) EXPAND_VARS(CONCAT, (MACRO, GET_NUM_ARGS(__VA_ARGS__)))(CONV, __VA_ARGS__)

#define EXPAND_CONV(CONV, ...) SELECT_MACRO_NUM(EXPAND_CONV_, CONV, __VA_ARGS__)

#define CONV_FLD(x) #x##_fld
#define GET_PLACEHOLDERS(x, ...)\
auto[__VA_ARGS__] =\
   x.GetPlaceholders(EXPAND_CONV(CONV_FLD, __VA_ARGS__))

#define CONV_NAMED x.named(#x)
#define NAMED_FLDS(...) EXPAND_CONV(CONV_NAMED , __VA_ARGS__)

これでGET_PLACEHOLDERNAMED_FLDSが出来上がった。引数の展開及び変換の部分はある程度汎用的に作ったので、類似のマクロを作成したい場合も数行書き足せば済むはずだ。
とはいえ、なんとも無駄の多い書き方になっている気がする。改善方法を知っている人がいたらぜひ教えてほしい。

閑話

毎日コードとにらめっこしているのにプログラミングしている気は全くしない。先人の構築したスパゲッティコードを修正する日々である。正直つまらない仕事なので、気分転換にと思ってちょっと作っていたのが本記事の機能だ。本当はもうちょっと工夫しないと実用的にならないのだが、今回は時間がないのでここまでで満足しておく。