C言語によるプリプロセッサメタプログラミング
目次
C言語でプリプロセッサメタプログラミング
C++などにはテンプレートメタプログラミングという型ごとにコードを生成する機能があります。
C言語はどうなのかと言うと、C言語の場合はプリプロセスで同様のことを実現できます。
これをプリプロセッサメタプログラミングと言って、これをやると少ないコードで複数の型に対応したコードが生成できます。
この記事ではC言語のプリプロセッサメタプログラミングについて解説します。
サンプルコード
まずはサンプルコードをご覧ください。
#include <stdio.h> #include <stdlib.h> #define DEF_ARRAY(NAME, TYPE)\ typedef struct NAME {\ TYPE *array;\ int len;\ int capa;\ } NAME;\ \ void\ NAME ## _del(NAME *self) {\ free(self->array);\ free(self);\ }\ \ NAME *\ NAME ## _new(int capa) {\ NAME *self = malloc(sizeof(NAME));\ self->len = 0;\ self->capa = capa;\ self->array = malloc((self->capa+1) * sizeof(TYPE));\ return self;\ }\ \ void\ NAME ## _resize(NAME *self, int new_capa) {\ self->array = realloc(self->array, (new_capa+1) * sizeof(TYPE));\ self->capa = new_capa;\ }\ \ void\ NAME ## _push_back(NAME *self, TYPE elem) {\ if (self->len >= self->capa) {\ NAME ## _resize(self, self->capa * 2);\ }\ self->array[self->len++] = elem;\ }\ \ TYPE\ NAME ## _get(const NAME *self, int index) {\ return self->array[index];\ }\ DEF_ARRAY(int_array, int); DEF_ARRAY(double_array, double); int main(void) { int_array *iary = int_array_new(2); int_array_push_back(iary, 1); int_array_push_back(iary, 2); int_array_push_back(iary, 3); int_array_push_back(iary, 4); int_array_push_back(iary, 5); for (int i = 0; i < iary->len; i++) { int elem = int_array_get(iary, i); printf("%d\n", elem); } int_array_del(iary); // double double_array *dary = double_array_new(2); double_array_push_back(dary, 1.1); double_array_push_back(dary, 2.2); double_array_push_back(dary, 3.3); double_array_push_back(dary, 4.4); double_array_push_back(dary, 5.5); for (int i = 0; i < dary->len; i++) { double elem = double_array_get(dary, i); printf("%f\n", elem); } double_array_del(dary); return 0; }
このコードを動的配列のライブラリをプリプロセッサメタプログラミングで生成しているところです。
main関数を見てみましょう。
main関数
まずmain関数の外で定義している以下のコードですが、
DEF_ARRAY(int_array, int); DEF_ARRAY(double_array, double);
これはint_array
というネームスペースでint
型の動的配列を。
double_array
というネームスペースでdouble
型の動的配列を定義しているところです。
int_array
int_array
のコードは以下になります。
int_array *iary = int_array_new(2); int_array_push_back(iary, 1); int_array_push_back(iary, 2); int_array_push_back(iary, 3); int_array_push_back(iary, 4); int_array_push_back(iary, 5); for (int i = 0; i < iary->len; i++) { int elem = int_array_get(iary, i); printf("%d\n", elem); } int_array_del(iary);
int_array
にはプリプロセッサメタプログラミングによって以下の関数が実装されます。
int_array_new() ... 動的配列オブジェクトの生成
int_array_push_back() ... 配列の末尾にデータを追加する
int_array_get() ... 配列のデータを添え字で得る
int_array_del() ... 動的配列オブジェクトを削除する
これらの関数はint_array
というネームスペースに関数の本体がくっついて生成されるだけです。
つまりint_array
という文字列の部分はDEF_ARRAY()
によって変えることができます。
double_array
double_array
のコードも見てみましょう。
やってることはint_array
のコードと同じです。
// double double_array *dary = double_array_new(2); double_array_push_back(dary, 1.1); double_array_push_back(dary, 2.2); double_array_push_back(dary, 3.3); double_array_push_back(dary, 4.4); double_array_push_back(dary, 5.5); for (int i = 0; i < dary->len; i++) { double elem = double_array_get(dary, i); printf("%f\n", elem); } double_array_del(dary);
しかしpush_back
するデータが浮動小数点数になっています。
これはdouble_array
がdouble
型の動的配列だからです。
このようにプリプロセッサメタプログラミングを使うとDEF_ARRAY()
とやっただけで色々なコードを生成できます。
DEF_ARRAYの中身
DEF_ARRAY()
の中身をちょっと見てみましょう。
#define DEF_ARRAY(NAME, TYPE)\ typedef struct NAME {\ TYPE *array;\ int len;\ int capa;\ } NAME;\ \ void\ NAME ## _del(NAME *self) {\ free(self->array);\ free(self);\ }\ \
DEF_ARRAY()
はdefine
で定義します。
引数のNAME
がネームスペース、つまりint_array
とかdouble_array
の識別子です。
そしてTYPE
が動的配列で扱う型になります。
構造体をNAME
で定義していますね。
またメンバのarray
のタイプもTYPE
で決定しています。
プリプロセスは単純な識別子の置き換えですからこのようなコードはたとえばDEF_ARRAY(int_array, int)
なら
typedef struct int_array { int *array; int len; int capa; } int_array;
という風に置き換えされます。
void\ NAME ## _del(NAME *self) {\ free(self->array);\ free(self);\ }\
というのは同様にNAME
でいろいろ置き換えていますが、
NAME ## _del(...)
という部分ではNAME
と_del
の識別子を##
で結合しています。
ですのでNAME
がint_array
であれば結合結果はint_array_del
になります。
このように##
を使うと識別子を合成させることができます。
モジュールの関数はすべてこの方法で関数名を生成しています。
あとは普通に関数を書いていけば複数の型に対応したコードのひな形ができます。
関数内で他の関数を呼び出すときは
void\ NAME ## _push_back(NAME *self, TYPE elem) {\ if (self->len >= self->capa) {\ NAME ## _resize(self, self->capa * 2);\ }\ self->array[self->len++] = elem;\ }\
という感じで呼び出す関数名もNAME
で合成します。
NAME ## _resize
というのがそれです。
おわりに
今回はC言語のプリプロセッサメタプログラミングについて解説しました。
プリプロセッサメタプログラミングができるようになると便利なライブラリの開発もはかどります。
なにか参考になれば幸いです。
(^ _ ^) | 型を抽象化! |
(・ v ・) | 型を流し込む |