3
0

More than 3 years have passed since last update.

C言語のdefineマクロを用いた構造体テンプレート化

Last updated at Posted at 2020-04-24

C++のテンプレートとは

C++のテンプレートは別々の型に対して似たような処理を行う場合に便利である。例えば、以下のように2次元ベクトル型を作り加法を定義する。

// Vector型の定義
template <class T>
struct Vector
{
    T x;
    T y;
    // 加法の定義
    Vector<T> operator+(const Vector<T>& v) const
    {
        return {x + v.x, y + v.y};
    }
};

int main()
{
    // 各要素がdouble型のVectorの例
    Vector<double> a{2.0, 1.0};
    Vector<double> b{4.0, -5.0};
    auto c = a + b;

    // 各要素がint型のVectorの例
    Vector<int> x{1, 2};
    Vector<int> y{5, 7};
    auto z = x + y;

    // 各要素がVector<int> 型のVectorの例
    Vector<Vector<int>> A{x,y};
    Vector<Vector<int>> B{y,x};
    auto C = A + B;
}

上の例ではdoubleのベクトルの足し算、intのベクトルの足し算、intのベクトルのベクトルの足し算を行っているが、それらの定義は一箇所に書かれている。このおかげでデバッグがしやすい、単調作業の量が減る、コードが簡潔になるといったメリットが生じる。templateは便利である。

C言語における障壁

C言語で上のようなコードを書く際には問題点がいくつかある。

- C言語ではテンプレートに対応していない
- 演算子オーバーロード(上でVectorの足し算を定めている方法)に対応していない

そこで、defineマクロを活用して上記の問題を解決する手法を提案する。その際、

- オーバーロードにも対応していない

という事実も少し厄介である。オーバーロードとは、同じ名前の関数を複数定義し、引数の型で選択を行う手法。

defineマクロとは

C言語のコードはコンパイルを行う前にプリプロセッサによって前処理が行われ、コードに変更が加えられている。#define A Bと書くと、プリプロセッサがコード内のABに置換する。こう書くと文字列置換のように思われるかもしれないが、関数のように#define f(X) (Xを含む式)とすることもできる。
例えば以下のようなコードが書ける。

#include <stdio.h>

#define A 200
#define f(x, y) x + y
#define g(number)                        \
    printf("number <- not replaced.\n"); \
    printf("number is %d\n", number)

int main()
{
    printf("number: %d\n", A);
    printf("x*y: %d\n", f(3, 19));
    int t = 40;
    g(t);
}

実行結果は以下のよう

number: 200
x*y: 22
number <- not replaced.
number is 40

例を見ればわかるように、複数行に渡る処理も可能である。
なお、defineマクロ自体はC++でも利用可能なのだが、エラーメッセージが読みにくくなるという問題点から一般に利用が推奨されていない。(エラーメッセージを出力するのはコンパイラであり、defineマクロを展開するプリプロセッサとは別物であるため)

追記 defineマクロでバグを生まない方法

コメント欄でfujitanozomuさんから指摘していただいたのだが、上のdefineのコードはdefineが単純な展開機能であるがゆえに生じるバグをはらんでいる。319を単純な数字ではなく12>>338>>2というように演算を含む形で書くとdefineマクロで展開する際にこの演算も含めて展開されてしまう。また、for文で関数gを回す際、{}で囲ってやらないとよくわからないところをリピートしてしまう。その例は以下のコード。

#include <stdio.h>
#define f(x, y) x + y
#define g(number)                        \
    printf("number <- not replaced.\n"); \
    printf("number is %d\n", number)

int main()
{
    printf("x*y: %d\n", f(12 >> 2, 38 >> 1));
    int i = 0;
    for (int i = 0; i < 5; i++)
        g(i);
}

実行結果は以下の通り。0にならないはずの足し算が0になったり、'number is'の行が繰り返されていなかったりという問題がある。

x*y: 0
number <- not replaced.
number <- not replaced.
number <- not replaced.
number <- not replaced.
number <- not replaced.
number is 5

この解決としては、x yをかっこで囲んだり、複数行をdo{...}while(0);の中に入れればよい

#include <stdio.h>

#define f(x, y) ((x) + (y))
#define g(number)                            \
    do {                                     \
        printf("number <- not replaced.\n"); \
        printf("number is %d\n", number);    \
    } while (0)

int main()
{
    printf("x*y: %d\n", f(12 >> 2, 38 >> 1));
    for (int t = 0; t < 5; t++)
        g(t);
}

実行結果

x+y: 22
number <- not replaced.
number is 0
number <- not replaced.
number is 1
number <- not replaced.
number is 2
number <- not replaced.
number is 3
number <- not replaced.
number is 4

無事バグが解消できた。(以上のバグと解決策の提示をしてくださあったのはfujitanozomuさんである。ありがとうございます。)

 構造体のテンプレート化をdefineマクロで実装

冒頭で紹介した構造体のテンプレート化を実装してみよう。文字列置換を用いいて演算のオーバーロード、関数のオーバーロードを擬似的に実装してやる。注意点として、C言語にはオーバーロードの機能がないためVector<double>,Vector<int>などにそれぞれ別の名前を定義しなければならない。また、関数のオーバーロードが行えないため ADDPRINTF などをdefineマクロを用いて展開することで要素ごとに利用する関数を切り替えている。

#include <stdio.h>
#include <stdlib.h>

#define def_vec(name, T, ADD, PRINTF, DELIMITER, \
    sum, print)                                  \
    typedef struct {                             \
        T x;                                     \
        T y;                                     \
    } name;                                      \
                                                 \
    name sum(name a, name b)                     \
    {                                            \
        name c;                                  \
        c.x = ADD(a.x, b.x);                     \
        c.y = ADD(a.y, b.y);                     \
        return c;                                \
    }                                            \
                                                 \
    void print(name a)                           \
    {                                            \
        PRINTF(a.x);                             \
        printf(DELIMITER);                       \
        PRINTF(a.y);                             \
    }

このようにして、テンプレート引数の代わりにdefineマクロの引数を色々入れてやることで似たような演算をまとめて書くことができそうである。(関数名や演算名が多数登場し頭がバグりそう。)
具体的なテンプレート引数を与えて構造体を宣言をする例を示す。

#define OPERATOR_PLUS(x, y)  ((x) + (y))
#define PRINT_DOUBLE(x) printf("%lf", x)
#define PRINT_INT(x) printf("%d", x)

def_vec(Vectord, double, OPERATOR_PLUS, PRINT_DOUBLE, " ", sum_vectord, print_vectord);
def_vec(Vector, int, OPERATOR_PLUS, PRINT_INT, " ", sum_vector, print_vector);
def_vec(Mat, Vector, sum_vector, print_vector, "\n", sum_mat, print_mat);

このようにして宣言した構造体の使用例を示す。冒頭のC++のコードに比べmain関数内のコード長が伸びているが、コンストラクタの行数が長く、出力部分を加えているためである。

int main()
{
    // doubleのvector
    Vectord a;
    a.x = 2.0;
    a.y = 1.0;
    Vectord b;
    b.x = 4.0;
    b.y = -5.0;
    Vectord c = sum_vectord(a, b);
    printf("c = \n");
    print_vectord(c);
    printf("\n\n");

    // intのvector
    Vector v1;
    v1.x = 1;
    v1.y = 2;
    Vector v2;
    v2.x = 5;
    v2.y = 7;
    Vector v3 = sum_vector(v1, v2);
    printf("v3 = \n");
    print_vector(v3);
    printf("\n\n");

    // vector<int>のvector
    Mat M1;
    M1.x = v1;
    M1.y = v2;
    Mat M2;
    M2.x = v2;
    M2.y = v1;
    Mat M3 = sum_mat(M1, M2);
    printf("M3 = \n");
    print_mat(M3);
    printf("\n\n");
}

実行結果

c = 
6.000000 -4.000000

v3 = 
6 9

M3 = 
6 9
6 9

追記 ##演算子について

コメント欄でご指摘いただいたのが、##演算子と呼ばれる機能がある。これはdefineマクロによって展開する際に文字列の結合を行えるものである。具体的には以下のようなコードが書ける。(ただしGCC拡張のようで、clangなどでは動作しないようである。)

#include <stdio.h>

#define new_int(num, value) int v##num = (value)

int main()
{
    new_int(0, 0);
    new_int(1, 10);
    new_int(a, -50);

    printf("%d\n", v0);
    printf("%d\n", v1);
    printf("%d\n", va);
}

##演算子によってv0,v1,vaという変数が宣言されており、実行結果は以下のようになる。

0
10
-50

これを用いて、テンプレート引数ごとに与えていた関数名を自動生成するように変更してしまおう。ついでに構造体の初期化をVectord a = {2.0, 1.0};と行うようにして、以下のように書き直すことができる。

#include <stdio.h>
#include <stdlib.h>

#define def_vec(name, T, ADD, PRINTF, DELIMITER) \
    typedef struct {                             \
        T x;                                     \
        T y;                                     \
    } name;                                      \
                                                 \
    name sum_##name(name a, name b)              \
    {                                            \
        name c;                                  \
        c.x = ADD(a.x, b.x);                     \
        c.y = ADD(a.y, b.y);                     \
        return c;                                \
    }                                            \
                                                 \
    void print_##name(name a)                    \
    {                                            \
        PRINTF(a.x);                             \
        printf(DELIMITER);                       \
        PRINTF(a.y);                             \
    }

#define OPERATOR_PLUS(x, y) ((x) + (y))
#define PRINT_DOUBLE(x) printf("%lf", x)
#define PRINT_INT(x) printf("%d", x)

def_vec(Vectord, double, OPERATOR_PLUS, PRINT_DOUBLE, " ");
def_vec(Vector, int, OPERATOR_PLUS, PRINT_INT, " ");
def_vec(Mat, Vector, sum_Vector, print_Vector, "\n");

int main()
{
    // doubleのvector
    Vectord a = {2.0, 1.0};
    Vectord b = {4.0, -5.0};
    Vectord c = sum_Vectord(a, b);
    printf("c = \n");
    print_Vectord(c);
    printf("\n\n");

    // intのvector
    Vector v1 = {1, 2};
    Vector v2 = {5, 7};
    Vector v3 = sum_Vector(v1, v2);
    printf("v3 = \n");
    print_Vector(v3);
    printf("\n\n");

    // vector<int>のvector
    Mat M1 = {v1, v2};
    Mat M2 = {v2, v1};
    Mat M3 = sum_Mat(M1, M2);
    printf("M3 = \n");
    print_Mat(M3);
    printf("\n\n");
}

まとめ

  • C言語のdefineマクロを利用してC++の構造体テンプレートに相当するものを実装することができた
3
0
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0