再入
マクロ展開は完全に関数的である。したがってイテレーションは存在しない。残念ながら、プリプロセッサは再帰も許していない。これはライブラリが、マクロを定義することによってイテレーションか再帰を模造しなければならないことを意味する。
例えば、これは単純な連結マクロである:
#define CONCAT(a, b) CONCAT_D(a, b)
#define CONCAT_D(a, b) a ## b
CONCAT(a, CONCAT(b, c)) // abc
これは上の例のように単純な例ではうまくゆくが、次のようなものでは何がおこるだろうか:
#define AB(x, y) CONCAT(x, y)
CONCAT(A, B(p, q)) // CONCAT(p, q)
再帰ができないため、この例はpq
ではなくCONCAT(p, q)
に展開される。
これを「修正」するには2つの方法しかない。
まず、AB
はCONCAT
を使っているため、上の例のような使いかたはできないことを文書化することができる。
次に、複数の連結マクロを定義することができる。
#define CONCAT_1(a, b) CONCAT_1_D(a, b)
#define CONCAT_1_D(a, b) a ## b
#define CONCAT_2(a, b) CONCAT_2_D(a, b)
#define CONCAT_2_D(a, b) a ## b
#define AB(x, y) CONCAT_2(x, y)
CONCAT_1(A, B(p, q)) // pq
これは問題を解決する。しかしながら、AB
は連結マクロとして特にCONCAT_2
を使っていることを知らなければならなくなった。
より良い解決法は、どの連結マクロが使われるかを抽象化することである...
#define AB(c, x, y) CONCAT_ ## c(x, y)
CONCAT_1(A, B(2, p, q)) // pq
これは総称的再入(generic reentrance)の例であり、この例では一連の連結マクロへの総称的再入になっている。
引数c
は連結の構成の「状態」を表現していて、ユーザがこの状態を憶えているかぎり、連結マクロの中からさらにAB
を使うことができる。
このライブラリは同様の選択肢を持っている。
その中で連結を構成することをを許さないか、あるいは複数の等価な定義を用意しその構成の中へ再入する均一の方法を提供している。
再帰を必要とする構成はいくつかある(たとえばBOOST_PP_WHILDE)。
その結果、このライブラリは、まだ使われていないマクロにおいてその集まりへ再入するメカニズムを伴ったマクロの集まりを提供することにしている。(Consequently, the library chooses to provide several sets of macros with mechanisms to reenter the set at a macro that has not already been used.)
特に、ライブラリはBOOST_PP_FOR、BOOST_PP_REPEAT、BOOST_PP_WHILEへの再入を提供しなければならない。
これを実現するには二つのメカニズムがある。状態パラメータ(上の構成の例のようなもの)と自動再帰(automatic recursion)である。
状態パラメータ
上の構成(BOOST_PP_FOR、BOOST_PP_REPEAT、BOOST_PP_WHILE)のいずれも、関連付けられた状態を持っている。
この「状態」はそれぞれの構成へ再入する手段を提供する。
いくつかのユーザ定義マクロはこれらの構成へ渡すことができる(述語や演算子として)。
ユーザ定義マクロが呼ばれるたびにそのユーザ定義マクロはそれを呼び出した構成の現在の状態を受け取り、必要ならそのマクロが(その構成へ)再入することができる。
これらの状態は、他のマクロへ連結するものと渡されるものの二つの方法のいずれかで利用される。
これらの状態パラメータが利用されるマクロには三つのタイプがある。
まず一つ目は、それ自身が連結を通じて再入するもの。
二つ目は、一つ目のタイプの一部となるようなもの。これらも連結を通じて再入する。
三つ目は、内部で一つ目や二つ目のタイプのマクロを利用しているもの。
それらは追加の引数として状態を取る。
BOOST_PP_WHILEの状態は文字Dによって表される。
述語および演算子の二つのユーザ定義マクロがBOOST_PP_WHILEに渡される。
BOOST_PP_WHILEがそれらユーザ定義マクロを展開する際には、それらがBOOST_PP_WHILDEへ再入できるように、状態がマクロへ渡される。
次の乗算の実装の例がその技を説明している:
// 「足し算」マクロ。
// 末尾の「_D」はこのマクロがBOOST_PP_WHILEへ再入することを表す。
#define ADD_D(d, x, y) \
BOOST_PP_TUPLE_ELEM( \
2, 0, \
BOOST_PP_WHILE_ ## d(ADD_P, ADD_O, (x, y)) \
) \
/**/
// BOOST_PP_WHILEへ渡される述語。
// 「y」が0にならない限り「true」を返し続ける。
#define ADD_P(d, xy) BOOST_PP_TUPLE_ELEM(2, 1, xy)
// BOOST_PP_WHILEへ渡される演算。
// 「y」が0にならない限り「x」がインクリメント、
// 「y」がデクリメントされる。いずれ「y」が0になり、
// 述語が「false」を返す。
#define ADD_O(d, xy) \
( \
BOOST_PP_INC( \
BOOST_PP_TUPLE_ELEM(2, 0, xy) \
), \
BOOST_PP_DEC( \
BOOST_PP_TUPLE_ELEM(2, 1, xy) \
) \
) \
/**/
// 乗算インタフェースマクロ。
#define MUL(x, y) \
BOOST_PP_TUPLE_ELEM( \
3, 0, \
BOOST_PP_WHILE(MUL_P, MUL_O, (0, x, y)) \
) \
/**/
// BOOST_PP_WHILEへ渡される述語。これは「y」が0に
// ならない限り「true」を返す。
#define MUL_P(d, rxy) BOOST_PP_TUPLE_ELEM(3, 2, rxy)
// BOOST_PP_WHILEへ渡される演算。これは「r」に「x」を
// 加え、「y」をデクリメントする。「y」が0になれば
// 述語が「false」を返す。
#define MUL_O(d, rxy) \
( \
ADD_D( \
d, /* pass the state on to ADD_D */ \
BOOST_PP_TUPLE_ELEM(3, 0, rxy), \
BOOST_PP_TUPLE_ELEM(3, 1, rxy) \
), \
BOOST_PP_TUPLE_ELEM(3, 1, rxy), \
BOOST_PP_DEC( \
BOOST_PP_TUPLE_ELEM(3, 2, rxy) \
) \
) \
/**/
MUL(3, 2) // 6へ展開される。
上の実装について注目すべきことが二つある。
まず、ADD_D
が状態パラメータdを使ってどのようにBOOST_PP_WHILEへ再入しているかを注意せよ。
次に、BOOST_PP_WHILEによって展開されるMUL
の演算がどのようにADD_D
へ状態を渡しているかを注意せよ。
これは引数によるものと連結によるものの二つの再入の実例である。
このライブラリのBOOST_PP_WHILEを使うマクロの各々について、状態再入を使う変種がある。
その変種がもし連結ではなく引数を使っているなら、それは_D
が名前の後ろにつく。
例えばライブラリに含まれるBOOST_PP_ADD_DやBOOST_PP_MUL_Dがその例である。
連結を使っているほうの変種は、名前の後ろにアンダースコアが付く。
それは「状態」を連結することによって完結する。
これにはBOOST_PP_WHILE自身(BOOST_PP_WHILE_ ## d)や、BOOST_PP_LIST_FOLD_LEFT(BOOST_PP_LIST_FOLD_LEFT_ ## d)が含まれる。
同様のものがBOOST_PP_FORとBOOST_PP_REPEATにもあるが、これらはそれぞれRとZの文字が状態を表すのに使われる。
上のMUL
の実装でさらに注意すべきことは、すぐにはわからないことだが、三つすべての種類の再帰を使っていることである。
二つの種類の状態再入だけでなく、自動再帰も使っている。
自動再帰
自動再帰は再帰の構成を非常に単純化する技である。
それは状態パラメータを単に使わないことによる。
上のMUL
の例は、それ自身がBOOST_PP_WHILEを使う際に自動再帰を使っている。
いいかえると、それはたとえBOOST_PP_WHILE_に状態を連結することによってBOOST_PP_WHILEへ再入していなくても、MUL
はまだBOOST_PP_WHILEの中で使うことができる。
In other words, MUL
can still be used inside BOOST_PP_WHILE even though it doesn't
reenter BOOST_PP_WHILE by concatenating the state to BOOST_PP_WHILE_.
これを実現するため、このライブラリはあるトリックを使っている。
見掛けとは違い、マクロBOOST_PP_WHILEは三つの引数は取らない。
実際、このマクロは全く引数を取らない。
そのかわり、BOOST_PP_WHILEマクロは、三つの引数を取るマクロへ展開される。
それは単に次のBOOST_PP_WHILE_ ## dマクロを見つけ、それを返す。
この「見つける」プロセスは入り組んでいて、私はそれがどのようにして動作しているかについては立ち入らないが、それが動作することさえ言えば十分である。
いくつかのマクロへ再入するのに自動再帰を使うのは明らかにずっと単純である。
それは実装の詳細を完全にかくしてしまう。
もしそうなら、なぜ状態パラメータを使うものが存在するのか?
理由は単純である。
状態パラメータが使われる時には、状態は常にわかる。
これは自動再帰の場合にはなりたたない。
自動再帰は状態が必要な場合にそれを推理しなければならない。
これはいくつかの状況においてマクロの複雑さ、とくにマクロの深さを増大させ、プリプロセッサを非常に遅くさせることになる。
結論
再入を行うのに状態パラメータを使うか自動再帰を使うかにはトレードオフがある。
自動再帰の利点は利用が簡単なことと実装が包み隠されることである。
しかし自動再帰はいくつかの状況においてプリプロセッサに性能上のコストを強いることになる。
状態パラメータの利点は逆に、効率性である。
状態パラメータの使用は最大の性能を得るのに唯一の方法である。
一方でそれはコードの複雑さと実装の暴露の問題がある。
参照
- Paul Mensonides