2012年8月9日木曜日

インスタンス関数のテーブル (table of instance function)

オブジェクト指向設計、プログラミング(Die objektorientierte design und programmierung)にとって重要な機構の 1つとして、汎化・特化(generalization - specialization)、あるいは継承(inheritance)がある。ここでは、継承を説明する前にその準備段階としてインスタンス関数をテーブル化する方法について説明する。 C++では仮想関数テーブル(vtable)と呼ばれているものである。 C++では仮想関数と非仮想関数を使い分けることができる。 softies projectの技法でも同様にこれらを使い分けることができるが、原則として全てのインスタンス関数は仮想関数であることが要求されている。

ヘッダファイル (header file)

これまでに何度か登場している Integerクラスを仮想関数に対応させるためのヘッダファイルをつぎに示す。
なお、前回実装した UpDownインターフェースは外している。
/* 4.7.9.Aug.2012       kei moses kissinger */

#ifndef _Included_Integer
#define _Included_Integer

typedef struct Integer* Integer;

typedef struct interface_Integer {
        void (*finalize)(Integer this);
        int (*getValue)(Integer this);
}* interface_Integer;

extern void static_Integer(interface_Integer interface_methods);

extern Integer new_Integer(int value);

extern void Integer_finalize(Integer this);

extern int Integer_getValue(Integer this);

#endif /* _Included_Integer */


仮想関数を使うためにヘッダファイルに必要になるのは interface_Integer構造体と、それを初期化するための static_Integer関数の2つだけである。

命名規則 (naming convention)では、クラス名の前にパッケージ名を付けると述べたが、この場合の接頭辞 interface, staticはパッケージ名ではなくキーワードである。 Cのキーワードとは異なる使い方のため、空白ではなく'_'で結合している。

また、関数テーブル interface_Integerは循環参照を使用しないことが明らかであるため、typedefと structの定義は分離していない。

ソースファイル (source file)

関数テーブルを使った Interfaceクラスのソースファイルを次に示す。
/* 4.7.9.Aug.2012       kei */

#include <Integer.h>

/****************************************************************
        private
****************************************************************/

#include <malloc.h>

struct Integer {
        interface_Integer interface_methods;

        int value;
};

static struct interface_Integer interface_methods;

static void finalize(Integer this) {
        free(this);
}

static int getValue(Integer this) {
        return this->value;
}

/****************************************************************
        public
****************************************************************/

void static_Integer(interface_Integer interface_methods) {
        /* ToDo: lock interface_methods */
        if (interface_methods->finalize == NULL) {

                interface_methods->finalize = finalize;
                interface_methods->getValue = getValue;
        }
}

Integer new_Integer(int value) {
        static_Integer(&interface_methods);

        Integer this = (Integer)malloc(sizeof(struct Integer));

        if (this != NULL) {
                this->interface_methods = &interface_methods;

                this->value = value;
        }

        return this;
}

void Integer_finalize(Integer this) {
        this->interface_methods->finalize(this);
}

int Integer_getValue(Integer this) {
        return this->interface_methods->getValue(this);
}


関数テーブルを使用することによるヘッダファイルの変更量は小さいが、ソースファイルはかなり複雑になっている。

struct Integerの先頭のメンバに interface_methodsが追加されている。これが C++のvtableであり、OMTで struct ShapeClass * class; と書かれているものとほぼ同じである。

外部 static変数 interface_methodsは関数テーブルの実体である。インスタンスそれぞれに関数テーブルを配することもできるが、インスタンスが多くなるとその関数テーブルのためのメモリもその数分必要になってしまう。そこでクラスごとに関数テーブルを1個だけとし、インスタンスにはそれへのポインタのみを持つようにすることでメモリ(内存, memory)を節約してる。 なお、関数テーブルを constとしないのには訳がある。クラスの継承を行うときのために、関数テーブルを静的に初期化するのではなく、1個ずつ設定できるようにするためである。これは継承の説明をするときに明らかにする。

2つの static関数 finalize, getValueは各々これまでの実装での Integer_finalize, Integer_getValueに記述されていた、機能の実装である。 代わりに Integer_finalize, Integer_getValueは関数テーブル経由で finalize, getValue関数を呼び出している。 これは一見無駄に見えるかも知れないが、継承による多相性(多態性: polymorphism)を実現するために重要な仕組みである。

static_Integer関数では、methods_tableの初期化を行っている。 外部 static変数の methods_tableを直接初期化するのではなく、わざわざ引数で渡して物を初期化しているのも継承クラスの初期化にも使用するためである。継承の例は次回お見せすることになる。

methods_tableの初期化は 1回だけで良いため、finalizeが nullのときだけ設定を行なう。

この実装では、複数の threadから同時に呼ばれたときに、競合が生ずる。特に継承した場合には深刻な問題を起こす可能性がある。実際にはロックが必要。

この static_Integerはコンストラクタ new_Integerの先頭で呼び出している。単純にするために、このようにコンストラクタから毎回呼び出してるが、最初に関数テーブルが使用されるよりも前に確実に、1回だけ呼び出すことができればオーバヘッドは解消される。この点はまだ良い方法を見つけておらず、今後の課題である。

そして、インスタンス変数初期化時にインスタンスメンバの interface_methodsポインタに、interface_methods実体のアドレスを設定する。

ビルドはつぎの通り。
$ make clean install
rm -f "libclass1.a";
rm -f *.o;
cc -g -I /export/home/kei/c/core/include -I /export/home/kei/c/experimental/include
 -I /export/home/kei/c/io/include -I /export/home/kei/c/lang/include -I /export/home/
kei/c/message/include -I /export/home/kei/c/unit/include -I /export/home/kei/c/util/
include -I /export/home/kei/c/x11/include -I ../../char/include -I ../../class1/
include -D_REENTRANT  -c -o Integer.o Integer.c
ar cvrf "libclass1.a" Integer.o ;
a - Integer.o
mkdir -p /export/home/kei/sub/class1/lib;
libclass1.a updated.
$ 


回帰テスト(regression test)はつぎの通り。
$ make clean test
cc -g -finstrument-functions -I. -I /export/home/kei/c/core/include -I /export/home/
kei/c/experimental/include -I /export/home/kei/c/io/include -I /export/home/kei/c/
lang/include -I /export/home/kei/c/message/include -I /export/home/kei/c/unit/
include -I /export/home/kei/c/util/include -I /export/home/kei/c/x11/include -I ../
../char/
include -I ../../class1/include  -c -o IntegerTest.o IntegerTest.c
# IntegerTest #
[CUnit] ./IntegerTest: testGetValue
[CUnit] ./IntegerTest: testNewAndFinalize
[SUCCEED] testGetValue in 3(ms)
[SUCCEED] testNewAndFinalize in 1(ms)
successes:    2 (100.0%)
failures:     0 (0.0%)
errors:       0 (0.0%)
total:        2 in 4(ms)
$ 


softies projectとは (what is softies project)

softiesは "Stupid Object Framework TechnologIES" を表したもの。

何ら新しい技術を用いているわけではないが、プログラミング言語 Cでオブジェクト指向プログラミングの基本機構の幾つかを実現している。UMLで設計したものを Cで実装できることを目標にしている。

まったく馬鹿げており、人手でコーディングするのは現実的では無いと思われ、C++を使用すべき状況である。しかし、ここで紹介している全ての手法を同時に用いるのではなく、期待する効果を絞り、そのために必要な方法だけを真似していただければきっと役に立つものと思われる。


小原日登美に続き伊調馨の金で君が代が流れ

0 件のコメント:

コメントを投稿