HN 日本語サマリー

← 一覧へ戻る
プログラミング

Cプリプロセッサの拡張を楽しむ

Fun Extending the C Preprocessor (davidpriver.com)

44 pointsby david2ndaccount13 コメント

要約

この記事では、Cプリプロセッサ(CPP)を拡張するいくつかの興味深い方法について解説しています。具体的には、デバッグに役立つ`__print()`や`__where()`マクロ、リストの各要素に変換を適用する`__map()`、そして可変長引数の数を直接取得できる`__VA_COUNT__`といった拡張機能を紹介しています。これらの拡張により、CPPの利用がより便利で、読みやすく、楽しくなることが示されています。

全文翻訳

Cプリプロセッサの拡張を楽しむ

以前の投稿や投稿で触れたように、私は個人的な楽しみのために独自のC2Yコンパイラ/インタプリタ/REPLを書いています。Cコンパイラを作成する際に最初に行うべきことの一つは、Cプリプロセッサ(CPPとも呼ばれます)を作成することです。多くの人がCプリプロセッサについて考えるのとは対照的に、それはテキストからテキストへの翻訳者ではありません。むしろ、ソースコードからCトークンへの翻訳者であり、ほとんどの実装ではその動作を確認するためのテキスト出力を提供しています。歴史的にはテキストからテキストへの翻訳者として実装されていたかもしれませんが、C標準では、Cパーサーが消費するCトークンに至る一連のフェーズとして指定されており、最終的にC翻訳単位が得られます。

他の誰かがCプリプロセッサの書き方についてすでに書いている可能性があるので、ここではそれに焦点を当てません。代わりに、使いやすくするため、あるいは単に面白いと思ったために、私がCプリプロセッサを拡張したいくつかの楽しい方法に焦点を当てます。すべてをリストアップするわけではありませんが、特に興味深いものだけを挙げます。

__print()/__where()

最も直接的な拡張機能:組み込みデバッグマクロです。

__print(...)はプリプロセッサのプリント関数として機能し、その引数を展開して標準エラー出力に書き込みます。マクロがどのように展開されるかをデバッグするのに役立ちます。__print()で正確に得られるものだけが表示されるため、cppのように翻訳単位全体をダンプするよりも、プリプロセッサがどのように展開されるかを簡単に確認できます。

$ printf "#define X 1\n__print(X   X)" | drcpp
(stdin):2:1: 1   1

奇妙な方法で展開されているマクロがどこで定義されているのか、特定するのに苦労したことはありませんか?もしかしたら、ばかげたglibcマクロがあなたの人生を台無しにしているのかもしれません。__where()があなたの味方です。マクロが定義されている場所を標準エラー出力に書き込みます。

$ printf "#define X 1\n\n\n\n__where(X)" | drcpp
(stdin):1:9: info: X defined

__map()

リスト内の各引数に何らかの変換を適用する必要が生じることがあります。例えば、プリプロセッサシーケンスにある値のシーケンスを出力したい場合などです。標準的な関数型のアプローチは、再帰関数(プリプロセッサでは関数形式マクロ)を書くことですが、C標準ではトークンが「青く塗られ」展開が抑制されるため、直接的な再帰はブロックされます。しかし、通常は任意の長さの再帰は必要ないため、固定回数のイテレーションで再帰を自分でシミュレートすることができます。もし嫌悪感を覚えるようなプリプロセッサの乱用を避けたいのであれば、この例は見ないでください:

警告しましたよ

// map.c
#define CAT(a, b) CAT_(a, b)
#define CAT_(a, b) a##b
#define NARG(...) \
  NARG_(__VA_ARGS__ __VA_OPT__(,) 8,7,6,5,4,3,2,1,0)
#define NARG_(_1,_2,_3,_4,_5,_6,_7,_8,N,...) N
#define MAP_0(m) 
#define MAP_1(m, x)      m(x) 
#define MAP_2(m, x, ...) m(x) MAP_1(m, __VA_ARGS__)
#define MAP_3(m, x, ...) m(x) MAP_2(m, __VA_ARGS__)
#define MAP_4(m, x, ...) m(x) MAP_3(m, __VA_ARGS__)
#define MAP_5(m, x, ...) m(x) MAP_4(m, __VA_ARGS__)
#define MAP_6(m, x, ...) m(x) MAP_5(m, __VA_ARGS__)
#define MAP_7(m, x, ...) m(x) MAP_6(m, __VA_ARGS__)
#define MAP_8(m, x, ...) m(x) MAP_7(m, __VA_ARGS__)
#define __map(m, ...) \
  CAT(MAP_, NARG(__VA_ARGS__))(m __VA_OPT__(,) __VA_ARGS__)
int x = 1;
int y = 2;
int z = 3;

#define PRINTINT(x) printf(#x " = %d\n", x);
int printf(const char*, ...);
int main(){
    __map(PRINTINT, 1, 2, 3, 4, x, y, z)
}

これは(読みやすさのために空白を挿入して)次のように展開されます:

// map.i
int x = 1;
int y = 2;
int z = 3;
int printf(const char*, ...);
int main(){
    printf("1" " = %d\n", 1); // 1 = 1
    printf("2" " = %d\n", 2); // 2 = 2
    printf("3" " = %d\n", 3); // 3 = 3
    printf("4" " = %d\n", 4); // 4 = 4
    printf("x" " = %d\n", x); // x = 1
    printf("y" " = %d\n", y); // y = 2
    printf("z" " = %d\n", z); // z = 3
}

これは便利ですが、明らかに非常に醜悪であり、プリプロセッサの展開を乱用しているため、各関数形式マクロの各引数を何度もスキャンして展開する必要があり、一般的に非常に遅いです。そのため、私のCプリプロセッサに__map(macro, ...)を追加しました:

組み込みの__map()を使用すると、上記の例は次のように単純化されます:

int x = 1;
int y = 2;
int z = 3;
#define PRINTINT(x) printf(#x " = %d\n", x);
int printf(const char*, ...);
int main(){
    __map(PRINTINT, 1, 2, 3, 4, x, y, z)
}

これで、再帰を偽装するために手間をかける必要がなくなり、マクロを各引数に線形スキャンで一度適用できます。そして、マップできる項目の数に任意の制限がなくなります!

__VA_COUNT__

C23では、__VA_OPT__()により可変長引数マクロの操作がより便利で強力になりましたが、一部のユースケースは依然として困難です。前のセクションで示したように、引数の数によってマクロを「オーバーロード」することは便利ですが、これには醜悪な手動でのプリプロセッサの操作が必要です。

別のユースケースは、コンパイラがまだCの最大の過ちを修正していない場合、カウント+ポインタを関数に渡すこと、または可変長関数にパラメータ数を渡すことです。

// sum.c
#define CAT(a, b) CAT_(a, b)
#define CAT_(a, b) a##b
#define NARG(...) \
  NARG_(__VA_ARGS__ __VA_OPT__(,) 8,7,6,5,4,3,2,1,0)
#define NARG_(_1,_2,_3,_4,_5,_6,_7,_8,N,...) N
int sum(size_t count, int* data){
    int result = 0;
    for(size_t i = 0; i < count; i++){
        result += data[i];
    }
    return result;
}
#define sum(...) sum(NARG(__VA_ARGS__), (int[]){__VA_ARGS__})
int printf(const char*, ...);
int main(){
    printf("%d\n", sum(1,2,3)); // 6
    printf("%d\n", sum(4,5)); // 9
}

このすべての無意味な手順を踏んで可変長引数の数を取得する代わりに(これはcppがすでに持っている機能です!)、組み込みマクロ__VA_COUNT__()を追加したので、可変長引数の数を直接取得できます。

// sum-drc.c
int sum(size_t count, int* data){
    int result = 0;
    for(size_t i = 0; i < count; i++){
        result += data[i];
    }
    return result;
}
#define sum(...) sum(__VA_COUNT__, (int[]){__VA_ARGS__})
int printf(const char*, ...);
int main(){
    printf("%d\n", sum(1,2,3)); // 6
    printf("%d\n", sum(4,5)); // 9
}

よりきれいで、より高速です!

結論

Cプリプロセッサは強力なツールですが、それを真に活用するには、読みにくい暗黒魔法が必要です。これは、やりたいことの本質ではありません。単に、未発達な機構の結果なのです。いくつかの拡張機能により、CPPをより強力にすると同時に、可読性を向上させ、より楽しく使用できるようにすることができます。

可変プリプロセッサ状態のような、真の暗黒魔法についてもさらに話すことはありますが、今回の投稿はここまでです!