C言語で動的型付けを実装する: 型の抽象化

289, 2021-07-14

目次

C言語で動的型付けを実装する

C言語は静的型付けの言語で、型は動的に決定されません。
しかし、C言語でも動的型付けのような仕組みが欲しい時があります。
たとえばコンパイラやインタプリタの実装です。
C言語で実装されているRubyやPythonは動的型付けが実装されています。
この動的型付けをどのように行うか、その一例をこの記事で解説します。

結論から言うと、動的型付けを実装したソースコードは↓になります。

この記事ではC言語による動的型付けについて具体的に↓を見ていきます。

  • 動的型付けとは?

  • C言語で動的型付けを行うメリット

  • Int, Floatオブジェクトの定義

  • Objectで型を抽象化

  • print()関数で抽象化したオブジェクトを出力

  • main関数を書く

動的型付けとは?

動的型付けとは、コンパイラやインタプリタが事前に(静的に)型を決定せずに、実行時に型を決定していく仕組みのことを言います。
インタプリタ系の言語では動的型付けが採用されている言語が多いです。
たとえばRuby, Python, PHPなど、これらの言語は実行時に型が動的に決定されます。

動的型付けと対称にあるのが静的型付けです。
これはコンパイラやインタプリタが事前に静的に型を決定する方法です。
コンパイラ型の言語、C/C++, Go, Rustなどはこの静的型付けが採用されています(ジェネリックという型を抽象化する機能もあります)。

C言語で動的型付けを行うメリット

最近(2021)は動的型付けに対する批判も検索結果では目立っているみたいです。
C言語は昔からある言語で、静的型付けが採用されていますが、このC言語で動的型付けの実装をする利点は何でしょうか。
1つは型を意識しないオブジェクトを作れるということになります。
型が動的に決定されるようにしておけば、型を意識しないでオブジェクトを扱うことが出来ます。
たとえば動的型付けを持つインタプリタの実装では必要不可欠な技術です。

あと1つは、私は昔からC言語を使っていて、多少静的型付けに飽きてしまっている所があるので、どちらかと言うと今は動的型付けのほうがプログラミングをしてて楽しいです。
つまり書き手の好みによっては、動的型付けの方が好まれる場合もあります。実用性ももちろんありますが。

Int, Floatオブジェクトの定義

私たちはインタプリタを実装しているという想定に立ちます。
言語の仕様にIntオブジェクトとFloatオブジェクトを追加することにしました。
これらのオブジェクトは動的型付けにより変数に値が束縛され、print()という汎用関数で値を出力することが出来ます。

C言語でこの仕様のインタプリタを作るには、まずIntオブジェクトとFloatオブジェクトを定義する必要があります。
その定義は↓のようになります。

// 整数オブジェクト
typedef struct {
    int value;
} Int;

// 浮動小数点オブジェクト
typedef struct {
    float value;
} Float;

IntオブジェクトとFloatオブジェクトは↑のようにともにvalue属性を持つ構造体です。
これでIntオブジェクトとFloatオブジェクトの定義は完了です。簡単ですね。

Objectで型を抽象化

次に先ほど定義したIntオブジェクトとFloatオブジェクトを、1つの型に抽象化するオブジェクトを定義します。
これはObjectという名前で定義します。

// 抽象オブジェクト
typedef struct {
    ObjectType type;
    void *real;
} Object;

ObjecttypeというObjectType型の変数と、void *型の変数を持っています。
ObjectType型の変数は、このObjectが何のオブジェクトであるか(realが何のオブジェクトなのか)を判別するために定義しています。
そしてvoid *型のrealIntオブジェクトやFloatオブジェクトのポインタを抽象化するための汎用ポインタです。

ObjectTypeの定義は↓のようになります。

// オブジェクトのタイプ
typedef enum {
    OBJ_TYPE_INT,
    OBJ_TYPE_FLOAT,
} ObjectType;

Objectの基本的な使い方はこうです。
まずtypeでオブジェクトの本当の型を判別します。これはOBJ_TYPE_INTOBJ_TYPE_FLOATになります。
つまりtypeOBJ_TYPE_INTだったらrealIntオブジェクト、typeOBJ_TYPE_FLOATだったらrealFloatオブジェクトになるということです。
そして型を判別したらrealを本当の型へキャストします。そうすることでIntオブジェクトやFloatオブジェクトのメンバ変数にアクセスすることが出来るようになります。
あとはそのキャストしたデータを使ってprint()などは画面に出力します。

print()関数で抽象化したオブジェクトを出力

ではprint()関数の実装を見てみます。
実装は↓のようになります。

/**
 * オブジェクトを出力する 
 */
void print(const Object *obj) {
    switch (obj->type) {
    case OBJ_TYPE_INT: {
        const Int *intobj = obj->real;
        printf("%d\n", intobj->value);
    } break;
    case OBJ_TYPE_FLOAT: {
        const Float *floatobj = obj->real;
        printf("%f\n", floatobj->value);
    } break;
    }
}

obj->typeで型を判別して、それぞれ本当の型へrealをキャスト後、その型のデータにアクセスしています。
これでprint()の実装は完了です。
考えられるバグとしては、typerealの値が合ってないというバグが想定されます。しかしこれはプログラマーが気をつければOKです。

main関数を書く

最後にmain関数を書きます。

int main(void) {
    // IntをObjectに抽象化
    Int intobj = { 123 };
    Object obj1 = { OBJ_TYPE_INT, &intobj };

    // FloatをObjectに抽象化
    Float floatobj = { 1.23 };
    Object obj2 = { OBJ_TYPE_FLOAT, &floatobj };

    // オブジェクトを出力する
    print(&obj1);  // 123
    print(&obj2);  // 1.230000

    return 0;
}

↑を見ると、obj1obj2の型が動的に決定されているのがわかります。
そしてprint()でこれらのオブジェクトは出力されます。ここも型が抽象化されているのがわかります。
つまりオブジェクトを出力したいのなら、print()にオブジェクトを渡せばあとは勝手にやってくれるわけです。

おわりに

今回はC言語による動的型付けの実装を見てみました。
インタプリタなどの実装の際に参考にしてみてください。

動的型付けはおもしろい

嫌ってる人も多いけどね