- 2021/07/04
- 載せたコードにミスがあったので修正
- 「サブクラス」、「スーパークラス」を「派生クラス」、「基底クラス」に統一
- 2022/08/05
- 「参照渡し」の誤用があったので修正
前にRustはオブジェクト指向型言語ではないという記事を書いた後で、Twitterで以下のような発言をしていた。
ふむ・・・今思ったのだが、CでOOPができると書いた手前CでOOPを実現すると言う記事を投稿していないのはどうかなと思った。
— Ukicode (@opaupafz2) 2021年6月12日
一度やったことがあるので、自信を持って「Cはオブジェクト指向型言語ではないがOOPは可能」と書いたが。
でも面倒なのよな。メソッドは簡単に実現できるけど、継承が。
今回はこれを有言実行しようと思う。
Cでオブジェクト指向プログラミング(以下OOP)をやっている例はほかにも存在しているのだが(これとかあれとか)、自分はあえてこれ以外のアプローチでOOPを実現する。
構造体を定義する
まずは構造体を定義する。ここまではほかの実現方法とほぼ一緒。
typedef struct { char name[256]; } object_t;
ほかのやり方では次にメソッドを定義するのだが、次に定義するのはメソッドではない。クラスである。
クラスを定義する
今回は関数をクラスに見立てることで、Alan Kay氏のオブジェクト指向におけるメッセージングにより近い表現でOOPを実現する。
void *Object(object_t *this, const char *msg, void *ret, va_list dap, ...) { /* メソッドはここで定義される */ }
もちろん、継承などもこれをもとに実現する。というか、今回の方法のほうが実は継承を実現しやすい(当社比)。実現方法については後述する。
ではそれぞれの意味について解説する。
まずvoid *
についてだが、これは汎用ポインタと呼ばれるものである。この型の変数は、いろいろな型のメモリアドレスを格納することができる優れものである。ただし汎用ポインタはあくまでvoid *
型であるため、特定の型の値を読み書きするためには型キャストが必要である。以下に例を示す。
#include <stdio.h> int main(void) { /* 型変換が必要な例 (1) */ { int var = 123; void *gp_ptr = &var; /* int型の値として読むためにint *型への変換が必要 */ printf("*(int *)gp_ptr == %d\n", *(int *)gp_ptr); } /* 型変換が必要な例 (2) */ { int var = 123; void *gp_ptr = &var; /* int型の値を書くためにint *型への変換が必要 */ *(int *)gp_ptr = 456; printf(" var == %d\n", var); } return 0; }
*(int *)gp_ptr == 123 var == 456
次に引数の一番最後は...
となっているが、これは省略したのではなく、立派なCの構文の一つである。可変長引数と呼ばれるもので、これにより変数をいくらでも引き渡すことが可能となる。
可変長引数を取り出すためにstdarg.h
に含まれるva_list
型とそれを使うためのマクロが必要である。以下に例を示す。
#include <stdio.h> #include <stdarg.h> void va_example(int arg_1, ...) { /* va_list型変数の定義 */ va_list ap; /* 可変長引数の開始(arg_1の次から開始される) */ va_start(ap, arg_1); /* char *型の引数として取り出す */ printf("%s\n", va_arg(ap, char *)); /* 可変長引数の終了 */ va_end(ap); } int main(void) { va_example(0, "Hello."); return 0; }
Hello.
以上をもとに、関数の型と引数を見てみよう。
void *Object(object_t *this, const char *msg, void *ret, va_list dap, ...)
これについてまとめた一覧表を以下に示す。
引数 | 型 | 説明 |
---|---|---|
this | object_t * | インスタンス。ポインタである理由はポインタの先の変数自体を変更する必要があるため。 |
msg | const char * | メッセージ。対応したメソッドを実行するために使う。 |
ret | void * | 戻り値。必要ない場合はNULL でも良い。クラス内で宣言したポインタを返さないようにする*1ためにその代替策として導入。 |
dap | va_list | 派生クラスから基底クラスの引数付きメソッドを使うために必要。使わない場合はNULL を渡す。 |
戻り値 | void * | あらゆるメソッドに適応した型を返すため汎用ポインタとしている。エラーはNULL 。 |
クラスにメソッド+αを実装する
ではクラスにメソッドとその他必要なものを実装しようと思う。
#include <stdio.h> #include <stdarg.h> #include <string.h> /* 構造体定義部分は省略 */ /* クラスの定義 */ void *Object(object_t *this, const char *msg, void *ret, va_list dap, ...) { void *ret_code = this; /* デフォルトではインスタンス自身を返す */ va_list ap; if (dap == NULL) { /* 可変長引数から取り出し開始 */ va_start(ap, dap); } else { /* 派生クラスの可変長引数を使う場合コピーする */ va_copy(ap, dap); } if (strcmp("new", msg) == 0) { /* コンストラクタ */ char *name = va_arg(ap, char *); strcpy(this->name, name); } else if (strcmp("hello", msg) == 0) { printf("Hello, %s!\n", this->name); } else if(strcmp("name", msg) == 0) { /* ゲッターメソッド。メンバ変数があるなら入れたほうが良い */ ret_code = &this->name; } else { /* エラー */ ret_code = NULL; } /* 可変長引数の取り出し終了 */ va_end(ap); return ret_code; }
人によっては眩暈を起こすかもしれないが、順番に解説していこう。
まず、可変長変数の取り出し開始の動作は以下の2つに分岐されている。
if (dap == NULL) { /* 可変長引数から取り出し開始 */ va_start(ap, dap); } else { /* 派生クラスの可変長引数を使う場合コピーする */ va_copy(ap, dap); }
*
がついていないため違和感を覚えるかもしれないがva_list
型はポインタである。そのため、NULL
を代入しても良い。故に、NULL
での判定が可能である。そもそも、この引数dap
は何に使うんだよ、と思われるかもしれないが、これは重要である。必要性については後述する。
この後に書かれているのは、皆さんお待ちかね(?)メソッドである。
if (strcmp("new", msg) == 0) { /* コンストラクタ */ char *name = va_arg(ap, char *); strcpy(this->name, name); } else if (strcmp("hello", msg) == 0) { printf("Hello, %s!\n", this->name); }
Cで書いたことがあるならわかると思うが、Cは文字列の扱いが苦手である。よって、文字列を比較するためにはstrcmp
関数が必要となる。この文字列比較によって文字列が等価だった場合に処理を実行することで、メソッドを実現することができる。
え、関数じゃないからメソッドじゃないって?メソッドが関数じゃなければならないと、誰が決めたのだろうか?メソッドはあくまでオブジェクトを操作する”手段”にすぎず、その実現方法が関数である必要はない。
ちなみに対応するメソッドがなかった場合、クラスはNULL
を返すようになっている。
else { /* エラー */ ret_code = NULL; }
次に見てほしいのがゲッターメソッドである。これは継承を実現する際にあったほうが便利である。理由はいずれわかるだろう。
else if(strcmp("name", msg) == 0) { /* ゲッターメソッド。メンバ変数があるなら入れたほうが良い */ ret_code = &this->name; }
これで粗方クラスとメソッドの書き方はわかったのではないだろうか。ではこれを使った例を以下に示す。
#include <stdio.h> #include <stdarg.h> #include <string.h> /* 構造体, クラス定義は省略 */ int main(void) { /* インスタンスの生成 */ object_t ins_1; /* インスタンスの初期化 */ /* ins_1.nameに"C"を代入している */ Object(&ins_1, "new", NULL, NULL, "C"); /* helloメッセージを送る */ /* "Hello, C!"が標準出力されるメソッドが実行される */ Object(&ins_1, "hello", NULL, NULL); /* メッセージに対応したメソッドがない場合、エラーになる */ if (Object(&ins_1, "abcd", NULL, NULL) == NULL) { printf("Class does not have \"abcd\" method.\n"); } return 0; }
Hello, C! Class does not have "abcd" method.
継承
では次に継承を実現してみる。面倒なので一気に見せる。
/** * 継承 */ /* object2_tの定義 */ typedef struct { object_t super; char derive_name[256]; } object2_t; /* super関数(基底クラスを使うための関数) */ void *object2_super(object2_t *this, const char *msg, void *ret, va_list ap) { return Object(&this->super, msg, ret, ap); } /* Object2クラスの定義 */ void *Object2(object2_t *this, const char *msg, void *ret, va_list dap, ...) { void *ret_code = this; va_list ap; if (dap == NULL) { /* 可変長引数から取り出し開始 */ va_start(ap, dap); } else { /* 派生クラスの可変長引数を使う場合コピーする */ va_copy(ap, dap); } if (strcmp("new", msg) == 0) { /* コンストラクタ */ char *derive_name = va_arg(ap, char *); object2_super(this, msg, NULL, ap); strcpy(this->derive_name, derive_name); } else if(strcmp("derive_name", msg) == 0) { /* ゲッターメソッド。メンバ変数があるなら入れたほうが良い */ ret_code = &this->derive_name; } else { /* 基底クラスを使う */ ret_code = object2_super(this, msg, ret, ap); } /* 可変長引数の取り出し終了 */ va_end(ap); return ret_code; }
ということで、ここから必要なところを取り出していく。
まずは構造体の定義。メンバ変数も含めて継承するためsuper
を定義している。
/* object2_tの定義 */ typedef struct { object_t super; char derive_name[256]; } object2_t;
次に、super関数であるobject2_super
を定義する。基底クラスを使うための関数である。ただし、必要ないため可変長引数はない。
/* super関数(基底クラスを使うための関数) */ void *object2_super(object2_t *this, const char *msg, void *ret, va_list ap) { return Object(&this->super, msg, ret, ap); }
そして最後に、Object2
クラスを見てほしい。特に注目してほしいのはこの2箇所である。
if (strcmp("new", msg) == 0) { /* コンストラクタ */ char *derive_name = va_arg(ap, char *); object2_super(this, msg, NULL, ap); strcpy(this->derive_name, derive_name); }
else { /* 基底クラスを使う */ ret_code = object2_super(this, msg, ret, ap); }
コンストラクタと対応したメソッドがない場合に先ほど定義したobject2_super
が呼び出されている。
ここでコンストラクタではObject2
のva_list
が渡されているされていることに気づいたのではないだろうか。
この渡したva_list
は最終的にはここで使われる。
/* クラスの定義 */ void *Object(object_t *this, const char *msg, void *ret, va_list dap, ...) { void *ret_code = this; /* デフォルトではインスタンス自身を返す */ va_list ap; if (dap == NULL) { /* 可変長引数から取り出し開始 */ va_start(ap, dap); } else { /* 派生クラスの可変長引数を使う場合コピーする */ va_copy(ap, dap); } /* 省略 */ }
そう、Object
のdap
引数に渡され、va_copy
マクロ*2によってコピーされる。したがって、派生クラスの可変長引数を基底クラスに渡すことが可能となる。これがdap
引数の用途である。
対応したメソッドがない場合に使われるsuper関数については、以下のコードを見たほうが早い。
#include <stdio.h> #include <stdarg.h> #include <string.h> /* 構造体, クラス定義は省略 */ int main(void) { /* インスタンスの生成 */ object2_t ins_2; /* インスタンスの初期化 */ /* ins_2.nameに"C"を、ins_2.derive_nameに"C++"を代入している */ Object2(&ins_2, "new", NULL, NULL, "C++", "C"); /* helloメッセージを送る */ /* Object2クラスに"hello"メソッドはなかったはずだが・・・? */ Object2(&ins_2, "hello", NULL, NULL); return 0; }
Hello, C!
もうわかったのではないだろうか。対応するメソッドがなかった場合、基底クラスからメソッドを探し、見つけたら実行している。つまり以下のようになっている。
以上から、継承を実現できていることがわかる。
そしてここからあることに気づいた人もいるだろう。派生クラスに基底クラスにもあったメソッドを追加してあげれば、なんとメソッドのオーバーライドも実現できる。
それを確かめるためにまた継承してみよう。
/** * 継承(2) */ /* object3_tの定義 */ typedef struct { object2_t super; } object3_t; /* object3_super関数は省略 */ /* Object3クラスの定義 */ void *Object3(object3_t *this, const char *msg, void *ret, va_list dap, ...) { void *ret_code = this; va_list ap; if (dap == NULL) { /* 可変長引数から取り出し開始 */ va_start(ap, dap); } else { /* 派生クラスの可変長引数を使う場合コピーする */ va_copy(ap, dap); } if (strcmp("new", msg) == 0) { /* コンストラクタ */ object3_super(this, msg, NULL, ap); } else if (strcmp("hello", msg) == 0) { /* メソッドのオーバーライド */ printf("Hello, %s and %s!\n", (char *)object3_super(this, "name", NULL, NULL), (char *)object3_super(this, "derive_name", NULL, NULL)); } else { /* 基底クラスを使う */ ret_code = object3_super(this, msg, ret, ap); } /* 可変長引数の取り出し終了 */ va_end(ap); return ret_code; }
Object3
クラスにObject
クラスにもあったhello
メソッドを追加した。
else if (strcmp("hello", msg) == 0) { /* メソッドのオーバーライド */ printf("Hello, %s and %s!\n", (char *)object3_super(this, "name", NULL, NULL), (char *)object3_super(this, "derive_name", NULL, NULL)); }
これを使ってみる。
#include <stdio.h> #include <stdarg.h> #include <string.h> /* 構造体, クラス定義は省略 */ int main(void) { /* インスタンスの生成 */ object_t ins_1; object3_t ins_3; /* インスタンスの初期化(Object) */ /* ins_1.nameに"C"を代入している */ Object(&ins_1, "new", NULL, NULL, "C"); /* インスタンスの初期化(Object3) */ /* ins_3.nameに"C"を、ins_3.derive_nameに"C++"を代入している */ Object3(&ins_3, "new", NULL, NULL, "C++", "C"); /* helloメッセージを送る(Object) */ Object(&ins_1, "hello", NULL, NULL); /* helloメッセージを送る(Object3) */ Object3(&ins_3, "hello", NULL, NULL); return 0; }
Hello, C! Hello, C and C++!
これでメソッドのオーバーライドも実現できることを確認できたのではないだろうか。
メンバ変数の継承について
実はこの継承方法、一つだけ問題点がある。それは、継承が深くなるごとに参照するメンバ変数も深くなることである。
instance.super.super.super.super./* ずっと続くよ! */.super.member;
深くなった場合、このようにしなければならない。当然だが、いちいちアクセスするのは非常に面倒である。
そこで、ゲッターメソッドの出番である。ゲッターメソッドがあれば、どれだけ継承が深くなっても、以下の記述だけで済む。
/* 変数の場合 */ *(T *)Class(&instance, "member", NULL, NULL); /* ポインタの場合 */ (T *)Class(&instance, "member", NULL, NULL);
ちなみに先ほどObject3
メソッドにてhello
メソッドをオーバーライドしたが、ここでもゲッターメソッドは活躍している。
else if (strcmp("hello", msg) == 0) { /* メソッドのオーバーライド */ printf("Hello, %s and %s!\n", (char *)object3_super(this, "name", NULL, NULL), (char *)object3_super(this, "derive_name", NULL, NULL)); }
これをゲッターメソッドなしで実装しようとすると、こうなる。
else if (strcmp("hello", msg) == 0) { /* メソッドのオーバーライド */ printf("Hello, %s and %s!\n", this->super.super.name, this->super.derive_name); }
この時点ではこちらのほうがきれいに見えるかもしれないが、継承が深くなるとこれどころではなくなるだろう。
そしてメンバ変数がどの構造体のメンバ変数なのか探る必要がないのもゲッターメソッドの利点と言える。故に、ゲッターメソッドはあったほうが良い。
終わりに
CでOOPについては以上である。まだ改善の余地はあると思われるが、Cでもオブジェクト指向プログラミングができなくもないことがわかったのではないだろうか。ここから、オブジェクト指向プログラミングができることは必ずしもオブジェクト指向型言語であることとイコールではないことがわかる。
さらに、たとえ様々な手法のプログラミングができてもプログラミング言語にそのパラダイムを持っていると断言するのは難しいとも言えるのではないだろうか。
もしかしたら「自分はこう断言しているけど、実はそのパラダイムを持っていなかった」ということもあるかもしれない。
この記事に共感できたという方はこれを機に「自分はこう思っていたけど、本当にそうなのか?」と疑問を持ってみてはいかがだろうか。