C言語によるプリプロセッサプログラミング
- 作成日: 2023-07-24
- 更新日: 2024-04-28
- カテゴリ: C言語
C言語でプリプロセッサプログラミング
C++などにはテンプレートプログラミングという型ごとにコードを生成する機能があります。
C言語はどうなのかと言うと、C言語の場合はプリプロセスで同様のことを実現できます。
これをプリプロセッサプログラミングと言って、これをやると少ないコードで複数の型に対応したコードが生成できます。
この記事ではC言語のプリプロセッサプログラミングについて解説します。
関連記事
C言語でcharをintに変換する方法
C言語でenumをtypedefして使う【列挙型】
C言語でforeachマクロを実装する方法
C言語でnull判定する方法【NULL, 比較】
C言語でできることを解説!C言語歴16年の開発者が語る
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言語のプリプロセッサプログラミングについて解説しました。
プリプロセッサプログラミングができるようになると便利なライブラリの開発もはかどります。
なにか参考になれば幸いです。
🦝 < 型を抽象化!
🦝 < 型を流し込む