Cでオブジェクト指向フレームワークを作る (3) 〜 C言語によるオブジェクト記述法「C~(シー・チルダ)」(原理編)
id:xoinu:20090507,id:xoinu:20090509,id:xoinu:20090511,に続いて,いよいよオブジェクト指向の記述を実践してみようと思います。
オブジェクト指向とは何かについては,Wikipediaを引いておきます。ここでは特徴として
が挙げられていますが,このうち,カプセル化はC言語でも普通にサポートされ,実践されている機能なので,C~では,多態と継承を実現することに焦点を当てます。言い換えると,C~の目的はC++の記述能力をシンプルなCで実現することです。
多くの人々が,C++をあたかもCの後継言語のように捉えられている背景には,言語そのものの機能ももさることながら,何より実行効率を意識した「実装」が優れているという点が大きいと思います。その思いから,C~では,通常はプログラマから隠蔽されているC++の仮想関数テーブルを,メソッドのディスパッチの一実装として採用しました。
ここからは,私の思考経路に沿って,C++のコードと,それに対してCでどう記述するか,というスタンスでC~の説明を試みることにします。
ポリシー
id:xoinu:20090507 にもありますが,C~の基本方針として以下の3つを挙げておきます。最後の1つは,仮想関数テーブルを採用する(C++からパクる)ことの正当化です。
- マクロに頼らない(言語の性質を尊重する)
- キャストを使用しない
- 性能に妥協しない
ポインタの定義については id:xoinu:20090509 を採用します。
クラスの定義
まずは単純に,メンバ変数が1つ,コンストラクタ,仮想デストラクタ,仮想関数がそれぞれ1つあるクラスを考えてみましょう。
class Atom { public: Atom(); virtual ~Atom(); virtual const char* typename() const; private: int x; };
C~では,これを2つの構造体で表します。1つは仮想関数テーブル,1つはデータ構造の定義です。まずは仮想関数テーブルから。
struct atom_vtbl { const char* (*type_name)(void); void (*finalize_)(union atom_ptr self); };
C++のAtomに仮想関数が2つあるのと同様,C~のatomにも,関数ポインタが2つあります。finalize_ の最後にアンダースコアがついているのは意図的で,private な定義であることを示唆しています。続いてデータ構造は以下のように表します。
struct atom { union { const struct atom_vtbl* vtbl; } su; int x; };
意味不明な union su がいますね。この構造体が「オブジェクト」としてインスタンス化されるわけですが,みそは,オブジェクトの最初のメンバは仮想関数へのポインタだということです。これを使うと,
//C++ Atom* a = ...; //何らかの方法でAtomをインスタンス化 a->typename();
というコードは
/*C*/ union atom_ptr a; /*何らかの方法でstruct atomをインスタンス化*/ a.self->su.vtbl->typename();
となります。メンバ関数の中でメンバ変数にアクセスする場合には,仮想関数テーブルを介して呼び出された関数の第1引数にa自身を入れる必要があります。
unionによる継承と多態の表現
次に継承と多態をいっぺんに実現します。上で定義した2つの構造体を原理的に同じ方法で拡張するので,分かり易いと思います。
//C++ class Blob : public Atom { public: virtual void foo() const; private: doouble foo_; };
Atomが拡張され,仮想関数とメンバがそれぞれ1つずつ追加されました。これに相当する構造体を表す前に,ポインタを定義します。id:xoinu:20090509 の考察に従って,
union blob_const_ptr { union atom_const_ptr atom; const struct blob* self; }; union blob_ptr { union atom_ptr atom; struct blob* self; union blob_const_ptr cnst; };
と書きます。blobのポインタをatomだと思って利用する時には b.atom でアクセスするわけです。次にやるべきことは「blobのポインタをatomだと思って利用」できるようにオブジェクトと,仮想関数のメモリイメージを揃えるだけです。
これに相当するものはやはり2つの構造体で表現します。まずは仮想関数テーブル:
struct blob_vtbl { struct atom_vtbl atom; const struct atom_vtbl* super; void (*bar)(union blob_ptr self); };
ここで,blob_vtbl(仮想関数テーブル)の最初のメンバがatom_vtblであることに注目。これによって,blob_vtblの先頭sizeof(struct atom_vtbl)バイトはatom_vtblと同じになっています。blob_vtblのatomを上書くことで,多態を実現しようという魂胆です。また,上書く前のオリジナルのメンバ関数にアクセスする時にはsuperを使います。次にデータ構造:
struct blob { union { const struct blob_vtbl* vtbl; struct atom atom; } su; double foo_; };
また,union su が出て来ました。「suって何だよ?」という疑問に答える前に,この定義のみそを書いておくと
- struct blob_vtblの先頭sizeof(struct atom_vtbl)バイトは,struct atom_vtblと互換性がある
- struct blobの先頭sizeof(struct atom)バイトは,struct atomと互換性がある
なので,
//C++ Blob* b = new Blob; Atom* a = b; a->typename();
のような呼び出しは
/*C*/ union blob_ptr b; union atom_ptr a; /*何らかの方法でbをインスタンス化*/ a = b.atom; a.self->su.vtbl->typename();
と書けるわけです。「キャストを使用しない」への解法が union だというのは少しずるいきもしますが,手続きを記述する最中に行うキャストよりも,クラスの設計時に用意する union の方が,遥かに安全なことには異論は無いでしょう。一方で
//C++ Blob* b = new Blob; b->typename();
という呼び出しを考えると
/*C*/ union blob_ptr b; : a.self->su.vtbl->atom.typename();
とかなって,ややこしいことになります。クラス階層が深くなるとなかなか興味深いことになりそうですが,書けるだけましだと言い聞かせましょう。