世間のプログラマのみなさんは、OOPを学ぶ際、 どんな経路で勉強されているんでしょうね?
ま、今時はJavaから入る人が多いのでしょうけど、 私がOOPに触れた頃は、まだ「これからはC++だ!!」という時代で、 でもオブジェクト指向自体の解説書はSmalltalk使ってたりして、 C++本の方はといえば、著者からして、 こいつクラスとインスタンスの区別が付いてないんじゃないの? というレベルの本が溢れてたりしました。
んで、しょうがないのでSmalltalkをベースにした本なんか読んでると、 「オブジェクト指向プログラミングでは、 オブジェクトが相互にメッセージを受け渡して、 協調動作しながらプログラムが実行される」 なんてことが書いてあったりして、
ふむふむ。オブジェクトが相互にメッセージを受け渡しすることにより プログラムが実行される、ということは、 オブジェクト指向では、オブジェクト毎にプロセス(またはスレッド)が 割り当てられているのかな?
なんてありがちな誤解をしてしまったりとかして、
# この誤解が本当に「ありがち」かどうかは知りませんけど、私は最初本当にそう # 誤解してしまいましたです。
おまけに、インヘリタンスとかポリモルフィズムとかいう聞きなれない 単語が出てきて、こりゃオブジェクト指向ってのは、 なんだかとてつもなく凄いものなのかなあ、なんて思ったものです。
でも、実際わかってみると、「メッセージの受け渡し」なんて 大仰な言葉を使っていたものは、実は単なる「関数呼び出し」でしかなく、 唯一違う所と言えば、最初の引数としてそのオブジェクトへのポインタを こっそり渡していることぐらいで、
「なんだ、結局、Cの標準入出力ライブラリが、 第1引数にFILE*を渡しているのと同じか」
と、えらく拍子抜けしたものです。
とはいえ、第1引数にオブジェクトへのポインタを渡しているだけでは、 継承が実現できませんし、継承には、メソッドオーバーライド (ポリモルフィズムとか仮想関数とか、 用語がバラバラなのはなんとかならんもんか)という重要な機能があります。
私の場合、その辺をはっきり理解したのは、C++によってでも Smalltalkによってでもなく、X Window SystemのXt Intrinsics(Athenaや Motifのようなウィジェットセットを構築するためのフレームワーク)に よってでした。Xtでは、 C言語で継承とメソッドオーバーライドを無理矢理実現している のです。
やっぱり、人間、低レベルなことの方が理解が早いわけで、 ポリモルフィズムがどうのレイトバインディングがどうのと、 なんやらよくわからん言葉を聞くよりも、 それが実際にどうすれば実現できるか、を知った方が、 少なくとも私のような「現場のおやじ」には手っ取り早いというものです。
というわけで、今回は、Xt等(GTK+も基本的に同じ技を使っていた筈)で 使用されているような、 「Cで無理矢理インヘリタンス」のテクニックを紹介しようと思います。
# 細かいところで、いろいろやり口を自己流にアレンジしてますけどね。
警告:
今回、「Cで無理矢理インヘリタンス」のテクニックを紹介したからといって、
Cで、この手の方法を使用することを「奨励」しているとは思わないでください。
Cでこういうことをやるのはあくまで裏技であって、
コーディングがキャストの嵐になる等の副作用が発生します。
もちろん、メリットとデメリットを秤にかけた上で、この手法を採用することを
止めはしませんが。実際、これが有用な局面もありますし。
継承の例題としてありがちなネタとして、 以下のような「Shape」を考えます。
ドローツールなどで使う例ですね。 ドローツールでは、たくさんのShape(形状)を書きますが、 それらはPolyline(折れ線)かもしれないし、Rectangle(矩形)かもしれないし、 Circle(円)かもしれないし、というデータ構造を表現しています。
# これだけ見れば、とってもありがちな例なんですが、ここに、例えば「Arc(弧)」を # 持ち込んだ時、CircleとArc、どっちがどっちのサブクラス? とか、Polygon(多角形) # とRectangle、どっちがどっちのサブクラス? とか、いろいろ悩む所はあるんですが、 # 取り敢えずそれはさておき。
これを、C言語で「まっとうな」書き方をするとしたら、 以下のように表現することになるのでしょう。
typedef enum { POLYLINE_SHAPE, RECTANGLE_SHAPE, CIRCLE_SHAPE } ShapeType; typedef struct { double x; double y; } Point; typedef struct { int num_points; Point *points; } Polyline; typedef struct { Point points[2]; } Rectangle; typedef struct { Point center; double radius; } Circle; typedef struct { Color color; /* 色 */ Point boundary[2]; /* その図形を含む最小の矩形領域 */ ... /* その他色々... */ ShapeType type; union { Polyline polyline; Rectangle rectangle; Circle circle; } u; } Shape;
説明するまでもないでしょうが、上記の例では、列挙型ShapeTypeにより、 共用体 u の中のどのメンバが有効かを識別するようにしています。
C言語を対象とする限りにおいて、おそらく、この方法が、 最も妥当な方法だろうと思います。
でも、C++やJavaなどの「継承」に比べて、この方法には以下の欠点があります。
3番目の問題については、逆にこれのおかげで「Shapeの配列」を 作ることが可能になっていると考えれば、 一概に欠点とは言えないかも知れませんけど。 継承を使ってこのような構造を構築した場合、 C++では、Shapeが仮想クラスでなかったとすれば、 Shapeの配列を作ることはできますが、そこにPolylineとかを格納しようとすると スライシングと呼ばれる恐ろしい現象が発生して Polylineの分の情報がぶっ飛びます。 また、Javaでは、そもそもオブジェクトの配列を作ることができません (オブジェクトへのポインタの配列なら作れるけど)。
それはさておき、 最初のふたつの問題は(基本的に同じこと言ってるような気もしますが(^^;) 確かに問題になり得るので、 C言語でこれを回避する方法を見ていこうと思います。
おおざっぱに言えば、「継承」とは、 スーパークラスに対してメンバを追加することです。
ですから、以下のようなShape構造体があったとき、
typedef struct { Color color; /* 色 */ Point boundary[2]; /* その図形を含む最小の矩形領域 */ ... /* その他色々 */ } Shape;
Shapeを継承してPolylineを作ろうと思ったら、以下のようになります。
typedef struct { Shape super; /* スーパークラスの分 */ int num_points; /* ここから下が自分の分 */ Point *points; } Polyline;
OOP的考え方では、継承は "is a" の関係であり、 Polyline is a Shape なので、Polylineは常にShapeとして扱うことが 可能でなければなりません。 これを C で実現するには、ポインタをキャストすることになります。
/* Polylineの取得 */ Polyline *polyline = malloc(sizeof(Polyline)); ... /* PolylineをShapeとして扱う */ Shape *shape = (Shape*)polyline;
ShapeとPolylineの間では、Shapeのメンバを見ている限りでは どうせオフセットは同一なので、 Polyline* をShape* にキャストして Shapeとして参照することが可能であるわけです。
ところでこれ、たいていの処理系でそのように扱えるだろうということは 予測できるのですが、規格の上ではどうなってるかといいますと、 6.5.2.1の「意味規則」に、以下のふたつの記述があります。
どうやら規格の上でも、この手法は認められるようです。 ただ、C++では構造体メンバが宣言の順に並ぶことは保証されてないようですし、 合法とはいえ「反則ギリギリ」なテクニックのような気はしますけれども。
ところで、この方法だと、継承が深くなっていくと、 ずーっと上の方のスーパークラスのメンバを参照するのに、
hoge.super.super.super.super.super.piyo;
のような記述をしなければなりません。
Xtでは、以下のような方法で、これを回避しています。
/* Shape固有の情報だけを格納する構造体 */ typedef struct { Color color; Point boundary[2]; } ShapePart; /* Shapeオブジェクトの構造体。内容はShapePartのみ */ typedef struct ShapeObj_tag { ShapePart shape; } ShapeObj; typedef struct ShapeObj_tag *Shape; /* Polyline固有の情報だけを格納する構造体 */ typedef struct { int num_points; Point *points; } PolylinePart; /* Polylineオブジェクトの構造体。ShapePartとPolylinePartを含む。 */ typedef struct PolylineObj_tag { ShapePart shape; PolylinePart polyline; } PolylineObj; typedef struct PolylineObj_tag *Polyline;
ShapeとPolylineはそれぞれ不完全型へのポインタになっているので、 パブリックヘッダファイルに置くんですけど、それについては後述します。
さて、これ、Xtではこういう手法を使っている、というのは事実なんですが、 これが規格上どうかということを考えますと、
規格では、構造体の先頭要素については、 ポインタをキャストして正しく取り出せることを保証していますが、 先頭以外の要素について、どのようにアライメントを行なうかは 処理系定義です。
ただ、同じ並びの構造体に対し、わざわざ異なるパディングを行なう 変態なコンパイラが存在するとは思えませんし、 6.3.2.3には「共用体が同一の先頭のメンバの並びを持つ幾つかの 構造体を持ち、共用体オブジェクトが現在それらの構造体の一つを 保持している場合、いずれかの構造体の同一の先頭のメンバの並びを 参照してもよい」と定められています。 構造体の各メンバのオフセットは、共用体のメンバになろうがなるまいが 一定している筈ですから、これで良いと言えば良いような気がします。
でも、やっぱり「反則ギリギリ」で反則側に片足突っ込んだ テクニックのように思うんだけどなあ...
さて、上記の方法で、データメンバに対しては継承らしきことが 実現されているわけですが、 継承を標傍するからには、やっぱりメソッドオーバーライドができなきゃ ダメでしょう。
メソッドオーバーライドというのは、例えば現在保持している全てのShapeを 画面に描画する時に、
for (全てのshapeについてブン回す) { shape->draw(); // C++の場合。Javaなら、shape.draw() }
このように書けるようにする機能のことです。
shapeは、実際にはPolyline, Rectangle, Circleのいずれかであって、 それぞれの図形毎に、draw()の実装は異なるのですが、 プログラム上では、上記のようにただshapeのdraw()を呼び出すだけで、 それぞれの図形に応じた描画ルーチンが自動的に呼び出されるわけです。
# メソッドオーバーライドについて、コンパイラが呼び出しの所にswitch caseを展開 # しているんだ、と思ってる人って、実は結構いそうな気がするんですけど、それは # 誤解ですからね。もし、そうだとしたら、さらに継承してサブクラスを増やす毎に # 呼び出し部分のコードを再コンパイルする必要があります。C++で考えるとわかりま # すけど、上記呼び出し部分はShapeを定義しているヘッダファイルにしか依存してない # からmakeが通りませんし、Javaなら、アプレットを作る度にjava.applet.Appletを # リコンパイルしなきゃ、ってことになっちゃいます。 ## ついでに... ## これはOOPの教科書にありがちな問題だけど、draw()なんてメソッドを本当にShapeに ## 突っ込んでいいかどうかは、かなり議論が分かれる所だと思うんだけどなあ。 ## 簡単なドローツールならともかく、まっとうなCADとかなら、んなもん絶対にShapeに ## 入れてはいけないと私は思うんですけど。
これを実現するには、それぞれのインスタンスが、 自分のクラスを知っていなければなりません。
そこで、クラスディスクリプタという概念を導入します。 クラスディスクリプタは、各クラスについてひとつずつ存在する staticな構造体です。
各インスタンスはそれぞれ、 クラスディスクリプタへのポインタを保持するようにします。 各インスタンスのサイズがポインタ1個分大きくなりますが、 これはしょうがないでしょう。
クラスディスクリプタを指すポインタは、 Shapeに入れるのもしっくりきませんから、全てのクラスのスーパークラスとして Coreクラスを定義しましょう。java.lang.Objectに相当するクラスですね。
/* Coreクラスのインスタンス構造体 */ typedef struct { CoreClassDescriptor *class_descriptor; } CorePart; typedef struct CoreObj_tag { CorePart core; } CoreObj; typedef struct CoreObj_tag *Core;
# いやさ、名前はJavaに合わせてObjectにしてもよかったんだけど、見本にした # XtがCoreクラスという名前を使っていたことと、何よりも「Objectクラス」なんて # クラスなんだかインスタンスなんだかよくわからん言葉なんであんまり使いたく # ないかなあ、と。
Coreクラスのクラスディスクリプタには、以下のようなものを格納します (詳細は後述)。
そして、これらのメンバは、ShapeやPolylineのクラスディスクリプタにも 存在する筈ですから、クラスディスクリプタも、 インスタンスの方の構造体と同じように、継承が可能な構造にしておきます。
/* Coreクラスのクラスディスクリプタ */ typedef struct { char *class_name; CoreClassDescriptor *super_class; int size_of_instance; void (*class_initializer)(void); void (*initializer)(Core p); void (*finalizer)(Core p); } CoreClassDescriptorPart; struct CoreClassDescriptor_tag { CoreClassDescriptorPart core; }; /************************************************************/ typedef struct CoreClassDescriptor_tag CoreClassDescriptor; extern CoreClassDescriptor *coreClass;
最後から2行目のtypedefでは、 例によって不完全型へのポインタを作っています。
最後のexternでは、クラスディスクリプタへのポインタを、 グローバル変数として公開しています。 これは、インスタンスをnewする時に使います。後述。
以後、Coreクラスと、 その周辺の関数群のことを「フレームワーク」と呼ぶことにします。 フレームワークは、Cで「無理矢理インヘリタンス」を実現するための 枠組みであり、ShapeにもPolylineにも依存しません。
ここまで出来てしまえば、メソッドオーバーライドは簡単です。
まず、Shapeのクラスディスクリプタの中に、 draw 関数へのポインタを置きます。
typedef struct { void (*draw)(Shape shape); } ShapeClassDescriptorPart; struct ShapeClassDescriptor_tag { CoreClassDescriptorPart core; ShapeClassDescriptorPart shape; }; /************************************************************/ typedef struct ShapeClassDescriptor_tag ShapeClassDescriptor; extern CoreClassDescriptor *shapeClass;
さらに、Shapeを継承して、PolylineとかRectangleとかCircleの クラスディスクリプタも作ります。
typedef struct { /* 特に追加するものもないんだけど、 * 書かないとコンパイラに怒られるんだこれが */ int dummy; } PolylineClassDescriptorPart; struct PolylineClassDescriptor_tag { CoreClassDescriptorPart core; ShapeClassDescriptorPart shape; PolylineClassDescriptorPart polyline; }; /************************************************************/ typedef struct PolylineClassDescriptor_tag PolylineClassDescriptor; extern CoreClassDescriptor *polylineClass;
さて、クラスディスクリプタは、クラス毎にひとつずつ存在し、 drawというメンバも独立して存在しますから、
それぞれ保持するようにあらかじめ設定しておきます。 初期化子を使って静的に設定してしまえば良いでしょう。
そうしておいて、Shapeを描画する関数draw_shape()で、 以下のように記述すれば、メソッドオーバーライドが実現できるわけです。
void draw_shape(Shape shape) { ShapeClassDescriptor *shape_class; /* インスタンスに対応するクラスディスクリプタを引っ張り出す。 */ shape_class = (ShapeClassDescriptor*)shape->core.class_descriptor; /* そこから描画関数へのポインタを引っ張り出して、コールする。 */ shape_class->shape.draw(shape); }
このコードがShapeにしか依存していない、すなわち、 Shapeのサブクラスを増やしても、 このコードについてはリコンパイルの必要がない、 ということに注意してください。
オブジェクトの構築についてですが、 C++では、スタック上にオブジェクトを確保したり、 static(静的)に確保したり、オブジェクトの配列を確保したり 別のクラスのメンバにオブジェクトを入れたりすることが比較的自由にできますが、 これを考え始めると話がムチャクチャややこしくなるので、 Java流に「オブジェクトはヒープにしか確保できない」ことにしましょう。
だとすると、たとえばPolylineクラスのインスタンスを取得したいと思ったら、
Polyline p = new_polyline();
とでも書くようにして、new_polylineの中でPolylineの領域をmalloc()し、 適切に初期化して返せば良さそうではあります。
が、その方法だと、Polylineの構築について、Polylineクラスの作者に全てを 委ねることになりますので、Polylineクラスの作者が、そのスーパークラスの メンバの初期化まできちんと行なってくれるだろうか、という問題が発生します。
いや、コンストラクタやデストラクタの呼び出しの際、 スーパークラスのメソッドを暗黙に呼ぶべきかどうかってのは 結構言語設計者のポリシーが出る所であって、 スーパークラスのコンストラクタの呼び出しは サブクラスの実装者に任せるというのも 充分ひとつのポリシーであろうとは思うんですけれども、 さすがにCoreのメンバ(クラスディスクリプタへのポインタ)だけは、 フレームワークがきっちり設定してやるべきだろうと思います。
というわけで、new_instance()という関数を、フレームワーク側で 提供することにします。使い方は以下の通り。
Polyline p = (Polyline)new_instance(polylineClass);
引数に渡しているのは、クラス名に見えますが、 実はクラスディスクリプタへのポインタ(グローバル変数)です。
戻り値をPolylineにキャストしなきゃいけないのは、 こりゃCを使う限りにおいてはしょうがないですね。
# Xtでは、クラスのインスタンスをみんなWidget型にしちゃっているからキャスト # しなくていいわけですが、プログラムの可読性を考えると、それもちょっと、という # 気がします。
実装側は、
Core new_instance(CoreClassDescriptor *descriptor) { Core p; p = malloc(descriptor->core.size_of_instance); /* coreのメンバ(ディスクリプタへのポインタ)の設定 */ p->core.class_descriptor = descriptor; /* インスタンスの初期化 */ initialize_instance(descriptor, p); return p; } static void initialize_instance(CoreClassDescriptor *descriptor, Core p) { if (descriptor->core.super_class != NULL) { initialize_instance(descriptor->core.super_class, p); } if (descriptor->core.initializer) { /* (あれば)イニシャライザをコール */ descriptor->core.initializer(p); } }
こんな感じにすれば良いでしょう。
さらに、インスタンスのnewをフレームワークで行なうことにより、 最初にそのクラスのインスタンスがnewされるタイミングを確実に捕捉できますから、 そこでクラスイニシャライザを呼び出すことが考えられます。
staticメンバですが、JavaでもC++でも、クラス名を直接指定する方法・ インスタンスを指定する方法のふたつの方法でアクセスすることができます。
クラス名を直接指定する方法では、 当然そのクラスのstaticメンバが取得できるのですが(当たり前だ)、 インスタンスを指定した場合でも、取得できるのは、 そのインスタンスの宣言時のクラスのメンバであって、 そのインスタンスが実行時にどんなクラスであるかどうかは関係ありません。
class Shape { static int hoge = 5; } class Polyline extends Shape { static int hoge = 10; } class Test { public static void main(String[] args) { Shape shape = new Polyline(); /* Polylineのインスタンスを指定して表示してるんだけど... */ System.out.println(shape.hoge); } } <実行結果> % java Test 5 ←表示されるのはあくまでShapeのメンバ
ということはつまり、インスタンスを指定してstaticメンバにアクセスしても、 その実、クラス名指定で書くのと同じことだということですね。
ちなみに、上記のTestクラスの、
System.out.println(shape.hoge)
を、
System.out.println(Shape.hoge)
に変えて、javapにかけたり、classファイルをcmpしたりしてみたところ、 全く同じバイトコードになっていることが確認できました。
ということは、staticなデータメンバにしろ、staticなメソッドにしろ、 名前空間の問題を除けば、 結局普通の関数やグローバル変数と何ら変わる所はないということですね。
Cでこれを実現するなら、 命名規則とヘッダファイルの切り分けで対処すれば良いでしょう。
ということは、 今回は、クラスディスクリプタを構造体で定義しましたけど、 Core以外のクラスがクラスディスクリプタに追加するメンバは、 どうせ「関数へのポインタ」だけだということになります。 ということは、 Cで書くなら関数の型チェックを通すために 構造体にしなければならなかったとしても、 コンパイラが機械語吐く時のことなどを考えるなら 配列にしても別段構わないということになります。 C++などでは、この「関数へのポインタへの配列」のことを、 vtableと呼んだりするようです。
余談ですが、Java仮想マシンの仕様書では、 invokevirtual(仮想メソッドの呼び出し命令)の所に、 以下のような記述があったりします。
解決済みのメソッドを表現しているコンスタント・プール・エントリは、 解決済みのクラスのメソッドテーブルへの符号なしindex、および非ゼロの 符号なし整数バイトnargsを保持している。
<中略> objectrefは参照型でなければならない。 indexは、objectrefの型を保持するクラスのメソッド・テーブルへの 添え字として使用される。<中略> その添え字にあるテーブル・エントリは、メソッド・コードへの直接参照と その修飾情報を保持している。
invokevirtualはコンスタントプールの インデックスをオペランドとして取りますから、 要するにこれは、 invokevirtualのオペランドからコンスタントプールを引いて、 そっからvtableのインデックスを引いて、 そいでメソッドコードへの直接参照(要するに関数へのポインタですな)を 得なさい、ということなのでしょう(素朴な実装では、ですが)。
ついでに、Javaにはinterfaceというものがあって、 こいつは多重継承が可能なわけですが、 invokeinterfaceの所には以下の記述があります。
コンスタント・プール中の添え字にある項目は、タグ CONSTANT_InterfaceMethodref、すなわちインタフェース名、 メソッド名、メソッドのディスクリプタへの参照を保持していなければならない。
<中略> メソッド・テーブル名とディスクリプタが、解決済みのコンスタント・プール ・エントリの名前とディスクリプタと同一であるメソッドをメソッド・テーブルから 検索する。
検索結果はメソッド・テーブル・エントリであり、それはインタフェース ・メソッドのコードへの直接参照、およびメソッドの修飾子情報を保持している。
なんか日本語壊れている気がしますが、 interfaceについては、インデックス一発ではなく、呼び出し時に検索する、 ということのようですね。だから、インタフェースの呼び出しは、 普通のメソッドの呼び出しに比べて 遅くなるわけでしょう(素朴な実装では、ですが)。
それはいいんですが、なんでこんな実装詳細が、 JVMの仕様書に書いてあるんだろう? というのは謎ですねえ。
さて、今回考えてみたShapeクラス一式の実装方針ですが、
Xtでは、基本的に、1クラスを、以下のファイルで構成しています。
Shapeでも、これに倣ってみました。 てなわけで、実装はこちら。
実装(HTML)tar + gzipで固めたものは、こちら。
ついでに... Xtでは、Javaで言う所のリフレクションもどきも実装しています。
Cで、何故リフレクションみたいな真似ができないかと言えば、 構造体のメンバは、 コンパイラを抜けた時点で全てオフセット参照に変換されてしまっていて、 メンバの「名前」が残っていないからです。
というわけで、メンバの名前と、そのメンバの型・構造体中でのオフセットの 対応表を作ってやれば、リフレクションのように、メンバの名前(文字列)でもって 外部からオブジェクトを操作することが可能になります。
typedef enum { INT_TYPE, DOUBLE_TYPE, STRING_TYPE } Type; typedef struct { char *name; Type type; int offset; } Member;
こんな構造体の配列を静的に作ってやって、 CoreクラスのクラスディスクリプタにMember*を追加してやればいいわけです。
offsetメンバの設定は、 offsetofマクロ(stdlib.hを参照のこと)を使って静的に行なうことができますね。
X Window Systemには、これを使用して、実行中のアプリケーションの ウイジェットの属性を動的に変更するユーティリティeditresが標準で入っています。
こうして見てみると、単一継承でいいのなら、オブジェクト指向とか 継承とかメソッドオーバライドとか言ったって、たいした物ではないですよね。
もちろん、原理的には単純でも、これをどう活用するかが設計者の腕の見せ所、 ということになるわけですが。
ただ、オブジェクト指向は決して魔法ではないし、 ついでに言えば別に万能の妙薬でもない、 現実のシステムの設計においては、あくまで「割と便利な道具」あたりの スタンスで考えたほうがいいようだなあ、なんてことを、 いつも思っていたりします。私の場合。
ひとつ上のページに戻る | ひとつ前の話 | ひとつ後の話 | トップページに戻る