C言語で動的型付けを実装する: 型の抽象化
目次
- C言語で動的型付けを実装する
- 動的型付けとは?
- C言語で動的型付けを行うメリット
- Int, Floatオブジェクトの定義
- Objectで型を抽象化
- print()関数で抽象化したオブジェクトを出力
- main関数を書く
- おわりに
C言語で動的型付けを実装する
C言語は静的型付けの言語で、型は動的に決定されません。
しかし、C言語でも動的型付けのような仕組みが欲しい時があります。
たとえばコンパイラやインタプリタの実装です。
C言語で実装されているRubyやPythonは動的型付けが実装されています。
この動的型付けをどのように行うか、その一例をこの記事で解説します。
この記事ではC言語による動的型付けについて具体的に↓を見ていきます。
動的型付けとは?
C言語で動的型付けを行うメリット
Int, Floatオブジェクトの定義
Objectで型を抽象化
print()関数で抽象化したオブジェクトを出力
main関数を書く
関連記事
C言語の動的配列のリサイズ方法
動的型付けとは?
動的型付けとは、コンパイラやインタプリタが事前に(静的に)型を決定せずに、実行時に型を決定していく仕組みのことを言います。
インタプリタ系の言語では動的型付けが採用されている言語が多いです。
たとえば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;
Object
はtype
というObjectType
型の変数と、void *
型の変数を持っています。
ObjectType
型の変数は、このObject
が何のオブジェクトであるか(real
が何のオブジェクトなのか)を判別するために定義しています。
そしてvoid *
型のreal
はInt
オブジェクトやFloat
オブジェクトのポインタを抽象化するための汎用ポインタです。
ObjectType
の定義は↓のようになります。
// オブジェクトのタイプ typedef enum { OBJ_TYPE_INT, OBJ_TYPE_FLOAT, } ObjectType;
Object
の基本的な使い方はこうです。
まずtype
でオブジェクトの本当の型を判別します。これはOBJ_TYPE_INT
かOBJ_TYPE_FLOAT
になります。
つまりtype
がOBJ_TYPE_INT
だったらreal
はInt
オブジェクト、type
がOBJ_TYPE_FLOAT
だったらreal
はFloat
オブジェクトになるということです。
そして型を判別したら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()
の実装は完了です。
考えられるバグとしては、type
とreal
の値が合ってないというバグが想定されます。しかしこれはプログラマーが気をつければ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; }
↑を見ると、obj1
やobj2
の型が動的に決定されているのがわかります。
そしてprint()
でこれらのオブジェクトは出力されます。ここも型が抽象化されているのがわかります。
つまりオブジェクトを出力したいのなら、print()
にオブジェクトを渡せばあとは勝手にやってくれるわけです。
おわりに
今回はC言語による動的型付けの実装を見てみました。
インタプリタなどの実装の際に参考にしてみてください。
(^ _ ^) | 動的型付けはおもしろい |
(・ v ・) | 嫌ってる人も多いけどね |