C言語によるプリプロセッサメタプログラミング

708, 2023-07-24

目次

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_arraydouble型の動的配列だからです。

このようにプリプロセッサメタプログラミングを使うと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の識別子を##で結合しています。
ですのでNAMEint_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 ・)

型を流し込む



この記事のアンケートを送信する