C言語で構造体にインターフェースを実装する
目次
C言語でインターフェースを実装する
インターフェースとは、クラスがある言語でクラスに対して実装する共通メソッドのことです。
インターフェースは共通化して使用することができます。
特定の共通インターフェースを実装しているクラスA, Bは、インターフェースを取り出すことで同じように扱うことができます。
C言語にはクラスもインターフェースも機能としてありません。
しかし、構造体と関数ポインタ、voidポインタを使うことで似たようなことは実現できます。
今回はこのC言語のインターフェースを実装してみます。
インターフェースは何の役に立つのか?
インターフェースの利用例としてはゲーム開発がわかりやすいです。
たとえば敵キャラのゴブリンとオークがいたとします。
これらのゴブリンとオークは別々に処理を書くと、面倒です。
たとえば自動で移動する処理なんかは、敵キャラをすべて配列にまとめておいてfor文で処理したいことがあります。
そういうときに、インターフェースの配列を作って、ゴブリンとオークのインターフェ―スへの参照を配列に保存します。
そしてfor文で一括で配列のインターフェースのメソッドを呼び出します。
このようにインターフェースを使うと共通のインターフェースを持っているオブジェクトの処理を一括で処理できるようになります。
ソースコード全文
今回の検証用コードは以下になります。
/* C言語で構造体にインターフェースを実装する例。 * License: MIT * Since: 2023-11-08 */ #include <stdio.h> #include <stdlib.h> // 動物インターフェースを表現する構造体 typedef struct { void *self; // オブジェクトのポインタ void (*walk)(void *); // 実装すべきメソッド void (*run)(void *); // 実装すべきメソッド } IAnimal; // 鳥インターフェースを表現する構造体 typedef struct { void *self; // オブジェクトのポインタ void (*fly)(void *); // 実装すべきメソッド } IBird; // インターフェースを実装した構造体 // カッコーは動物かつ鳥 typedef struct { IAnimal ianimal; // IAnimalインターフェース IBird ibird; // IBirdインターフェース int health; } Cuckoo; // インターフェースを実装した構造体 // ライオンは動物 typedef struct { IAnimal ianimal; // IAnimalインターフェース int health; } Lion; // Cuckoo void cuckoo_walk(void *_self) { Cuckoo *self = (Cuckoo *) _self; self->health -= 10; printf("bird walk %d\n", self->health); } void cuckoo_run(void *_self) { Cuckoo *self = (Cuckoo *) _self; self->health -= 30; printf("bird run %d\n", self->health); } void cuckoo_fly(void *_self) { Cuckoo *self = (Cuckoo *) _self; self->health -= 40; printf("bird fly %d\n", self->health); } // デストラクタ void cuckoo_del(Cuckoo *self) { free(self); } // コンストラクタ Cuckoo *cuckoo_new(void) { Cuckoo *self = malloc(sizeof(*self)); self->health = 100; self->ianimal.self = self; self->ianimal.walk = cuckoo_walk; // インターフェースの実装 self->ianimal.run = cuckoo_run; // インターフェースの実装 self->ibird.self = self; self->ibird.fly = cuckoo_fly; // インターフェースの実装 return self; } // Lion void lion_walk(void *_self) { Lion *self = (Lion *) _self; self->health -= 10; printf("lion walk %d\n", self->health); } void lion_run(void *_self) { Lion *self = (Lion *) _self; self->health -= 30; printf("lion run %d\n", self->health); } // デストラクタ void lion_del(Lion *self) { free(self); } // コンストラクタ Lion *lion_new(void) { Lion *self = malloc(sizeof(*self)); self->health = 100; self->ianimal.self = self; self->ianimal.walk = lion_walk; // インターフェースの実装 self->ianimal.run = lion_run; // インターフェースの実装 return self; } // インターフェースを利用する関数 // BirdとLionの共通のインターフェースIAnimalのメソッドを呼び出す void func(IAnimal *impl) { impl->walk(impl->self); impl->run(impl->self); } // インターフェースを利用する関数 // CuckooのインターフェースIBirdのメソッドを呼び出す void func2(IBird *impl) { impl->fly(impl->self); } int main(void) { // オブジェクトの作成 Cuckoo *cuckoo = cuckoo_new(); Lion *lion = lion_new(); // IAnimalインターフェースの利用 func(&cuckoo->ianimal); func(&lion->ianimal); // IBirdインターフェースの利用 func2(&cuckoo->ibird); // オブジェクトの削除 cuckoo_del(cuckoo); lion_del(lion); return 0; } /* 実行結果 bird walk 90 bird run 60 lion walk 90 lion run 60 bird fly 20 */
ソースコードの解説
インターフェース
// 動物インターフェースを表現する構造体 typedef struct { void *self; // オブジェクトのポインタ void (*walk)(void *); // 実装すべきメソッド void (*run)(void *); // 実装すべきメソッド } IAnimal; // 鳥インターフェースを表現する構造体 typedef struct { void *self; // オブジェクトのポインタ void (*fly)(void *); // 実装すべきメソッド } IBird;
今回のC言語におけるインターフェースは構造体と関数ポインタで実現します。
上記のIAnimal
は動物に関する関数をまとめたインターフェース。
IBird
は鳥に関する関数をまとめたインターフェースです。
IAnimal
とIBird
にはともにself
というvoid型のポインタを持っています。
これにはインターフェースを持っている構造体のポインタが格納されます。
void型になっているのは、型を抽象化するためです。
IAnimal
のwalk
とrun
は関数ポインタです。
これがインターフェースで実装すべきメソッドに当たります。
メソッドとはクラスに属する関数のことを言います。
IBIrd
のfly
も関数ポインタになります。
IAnimal
とIBird
の名前の先頭のI
はInterface
のI
です。
インターフェースを構造体に実装する
// インターフェースを実装した構造体 // カッコーは動物かつ鳥 typedef struct { IAnimal ianimal; // IAnimalインターフェース IBird ibird; // IBirdインターフェース int health; } Cuckoo; // インターフェースを実装した構造体 // ライオンは動物 typedef struct { IAnimal ianimal; // IAnimalインターフェース int health; } Lion;
今回はCuckoo
(カッコー)とLion
(ライオン)という構造体を2つ作ります。
Cuckoo
にはIAnimal
とIBird
のインターフェースを持たせます。
Lion
にはIAnimal
のインターフェースを持たせます。
この実装はCuckoo
とLion
が共通のIAnimal
というインターフェースを持っています。
ですので、これら2つの構造体は共通化して処理できるということになります。
共通化して処理というのは、ianimal
のインターフェースを関数に渡せば、そのianimal
のインターフェースのメソッドを型に関係なく呼び出せる、ということです。
health
は体力を表す整数です。
Cuckooの実装
// Cuckoo void cuckoo_walk(void *_self) { Cuckoo *self = (Cuckoo *) _self; self->health -= 10; printf("bird walk %d\n", self->health); } void cuckoo_run(void *_self) { Cuckoo *self = (Cuckoo *) _self; self->health -= 30; printf("bird run %d\n", self->health); } void cuckoo_fly(void *_self) { Cuckoo *self = (Cuckoo *) _self; self->health -= 40; printf("bird fly %d\n", self->health); } // デストラクタ void cuckoo_del(Cuckoo *self) { free(self); } // コンストラクタ Cuckoo *cuckoo_new(void) { Cuckoo *self = malloc(sizeof(*self)); self->health = 100; self->ianimal.self = self; self->ianimal.walk = cuckoo_walk; // インターフェースの実装 self->ianimal.run = cuckoo_run; // インターフェースの実装 self->ibird.self = self; self->ibird.fly = cuckoo_fly; // インターフェースの実装 return self; }
Cuckoo
の実装ではデストラクタとコンストラクタを定義します。
デストラクタは破棄用の関数でコンストラクタは構築用の関数です。
コンストラクタでは、インターフェースのメソッド(関数ポインタ)に定義した関数を代入しておきます。
上記の例ではianimal.walk
にcuckoo_walk
という関数を入れています。
ここで代入を忘れるとインターフェースが機能しませんので注意が必要です。
またianimal.self
には、コンストラクタで動的にメモリを確保しているself
のアドレスを入れておきます。
これはインターフェースのメソッドを呼び出すときはこのself
を関数に渡してください、という表現になります。
cuckoo_walk
やcuckoo_run
関数では、引数のvoid型ポインタのself
を型キャストしています。
Cuckoo *self = (Cuckoo *) _self;
こうすることでvoid型のポインタがCuckko
のポインタとして機能するようになります。
つまりメンバ変数などにアクセスできるようになります。
Lionの実装
// Lion void lion_walk(void *_self) { Lion *self = (Lion *) _self; self->health -= 10; printf("lion walk %d\n", self->health); } void lion_run(void *_self) { Lion *self = (Lion *) _self; self->health -= 30; printf("lion run %d\n", self->health); } // デストラクタ void lion_del(Lion *self) { free(self); } // コンストラクタ Lion *lion_new(void) { Lion *self = malloc(sizeof(*self)); self->health = 100; self->ianimal.self = self; self->ianimal.walk = lion_walk; // インターフェースの実装 self->ianimal.run = lion_run; // インターフェースの実装 return self; }
Lion
についても同様に実装します。
インターフェースを利用する関数
// インターフェースを利用する関数 // BirdとLionの共通のインターフェースIAnimalのメソッドを呼び出す void func(IAnimal *impl) { impl->walk(impl->self); impl->run(impl->self); } // インターフェースを利用する関数 // CuckooのインターフェースIBirdのメソッドを呼び出す void func2(IBird *impl) { impl->fly(impl->self); }
上記の関数は実際にインターフェースを利用する関数です。
func()
ではIAnimal
のインターフェースを受け取り、IAnimal
のメソッドであるwalk
とrun
を呼び出しています。
このときに、walk
等の関数の引数にはimpl->self
を渡しておきます。
func2()
でも同様です。
main関数の実行
int main(void) { // オブジェクトの作成 Cuckoo *cuckoo = cuckoo_new(); Lion *lion = lion_new(); // IAnimalインターフェースの利用 func(&cuckoo->ianimal); func(&lion->ianimal); // IBirdインターフェースの利用 func2(&cuckoo->ibird); // オブジェクトの削除 cuckoo_del(cuckoo); lion_del(lion); return 0; }
main関数ではCuckoo
とLion
をオブジェクトにしてインターフェスを利用して処理します。
func()
やfunc2()
にオブジェクトが持つインターフェースのアドレスを渡します。
これらのコードを実行すると以下のような結果になります。
bird walk 90 bird run 60 lion walk 90 lion run 60 bird fly 20
おわりに
今回はC言語でインターフェースを実装してみました。
何か参考になれば幸いです。