ばかげているように聞こえるかもしれませんが、C がオブジェクト指向言語ではないこともわかっています。
しかし、C で動的メソッド ディスパッチを実現する方法はありますか? 関数ポインターについて考えましたが、全体のアイデアがわかりません。
どうすればこれを実装できますか?
ばかげているように聞こえるかもしれませんが、C がオブジェクト指向言語ではないこともわかっています。
しかし、C で動的メソッド ディスパッチを実現する方法はありますか? 関数ポインターについて考えましたが、全体のアイデアがわかりません。
どうすればこれを実装できますか?
他の人が指摘したように、これを C で実装することは確かに可能です。可能であるだけでなく、かなり一般的なメカニズムです。最も一般的に使用される例は、おそらく UNIX のファイル記述子インターフェイスです。ファイル記述子のread()
呼び出しは、そのファイル記述子を提供したデバイスまたはサービスに固有の読み取り関数にディスパッチされます (ファイルでしたか? ソケットでしたか? 他の種類のデバイスでしたか?)。
唯一の秘訣は、抽象型から具象型へのポインターを回復することです。ファイル記述子の場合、UNIX はその記述子に固有の情報を含むルックアップ テーブルを使用します。オブジェクトへのポインターを使用している場合、インターフェイスのユーザーが保持するポインターは、「派生」型ではなく「基本」型です。C には継承自体がありませんが、 a の最初の要素struct
へのポインターが、それを含む のポインターと等しいことが保証されますstruct
。したがって、これを使用して、「ベース」のインスタンスを「派生」の最初のメンバーにすることにより、「派生」タイプを回復できます。
スタックを使用した簡単な例を次に示します。
struct Stack {
const struct StackInterface * const vtable;
};
struct StackInterface {
int (*top)(struct Stack *);
void (*pop)(struct Stack *);
void (*push)(struct Stack *, int);
int (*empty)(struct Stack *);
int (*full)(struct Stack *);
void (*destroy)(struct Stack *);
};
inline int stack_top (struct Stack *s) { return s->vtable->top(s); }
inline void stack_pop (struct Stack *s) { s->vtable->pop(s); }
inline void stack_push (struct Stack *s, int x) { s->vtable->push(s, x); }
inline int stack_empty (struct Stack *s) { return s->vtable->empty(s); }
inline int stack_full (struct Stack *s) { return s->vtable->full(s); }
inline void stack_destroy (struct Stack *s) { s->vtable->destroy(s); }
ここで、固定サイズの配列を使用してスタックを実装したい場合は、次のようにすることができます。
struct StackArray {
struct Stack base;
int idx;
int array[STACK_ARRAY_MAX];
};
static int stack_array_top (struct Stack *s) { /* ... */ }
static void stack_array_pop (struct Stack *s) { /* ... */ }
static void stack_array_push (struct Stack *s, int x) { /* ... */ }
static int stack_array_empty (struct Stack *s) { /* ... */ }
static int stack_array_full (struct Stack *s) { /* ... */ }
static void stack_array_destroy (struct Stack *s) { /* ... */ }
struct Stack * stack_array_create () {
static const struct StackInterface vtable = {
stack_array_top, stack_array_pop, stack_array_push,
stack_array_empty, stack_array_full, stack_array_destroy
};
static struct Stack base = { &vtable };
struct StackArray *sa = malloc(sizeof(*sa));
memcpy(&sa->base, &base, sizeof(base));
sa->idx = 0;
return &sa->base;
}
そして、代わりにリストを使用してスタックを実装したい場合:
struct StackList {
struct Stack base;
struct StackNode *head;
};
struct StackNode {
struct StackNode *next;
int data;
};
static int stack_list_top (struct Stack *s) { /* ... */ }
static void stack_list_pop (struct Stack *s) { /* ... */ }
static void stack_list_push (struct Stack *s, int x) { /* ... */ }
static int stack_list_empty (struct Stack *s) { /* ... */ }
static int stack_list_full (struct Stack *s) { /* ... */ }
static void stack_list_destroy (struct Stack *s) { /* ... */ }
struct Stack * stack_list_create () {
static const struct StackInterface vtable = {
stack_list_top, stack_list_pop, stack_list_push,
stack_list_empty, stack_list_full, stack_list_destroy
};
static struct Stack base = { &vtable };
struct StackList *sl = malloc(sizeof(*sl));
memcpy(&sl->base, &base, sizeof(base));
sl->head = 0;
return &sl->base;
}
struct Stack *
スタック操作の実装は、それがあるべきだとわかっているものに単純にキャストします。例えば:
static int stack_array_empty (struct Stack *s) {
struct StackArray *sa = (void *)s;
return sa->idx == 0;
}
static int stack_list_empty (struct Stack *s) {
struct StackList *sl = (void *)s;
return sl->head == 0;
}
スタックのユーザーがスタック インスタンスでスタック操作を呼び出すと、その操作は 内の対応する操作にディスパッチされますvtable
。これvtable
は、その特定の実装に対応する関数を持つ作成関数によって初期化されます。そう:
Stack *s1 = stack_array_create();
Stack *s2 = stack_list_create();
stack_push(s1, 1);
stack_push(s2, 1);
stack_push()
と の両方で呼び出されs1
ますs2
。ただし、 のs1
場合は に発送しstack_array_push()
、 のs2
場合は に発送しstack_list_push()
ます。
C++ は (当初) C の上に構築されていました。最初の C++ コンパイラは、実際には中間ステップとして C を生成していました。したがって、可能です。
C++ がこのようなことを行う方法を次に示します。
オンラインには確かな情報がたくさんあります。ここに数分で書ききれないほどの情報があります。「グーグルとあなたが見つけるでしょう。」
あなたは上のコメントで言った:
誰かがすでに c で書かれたコードを持っていて、何らかの機能を追加することを好むでしょう。OOlanguage を使用してゼロから作成する代わりに。
このような機能を C で実現するには、基本的にオブジェクト指向言語の機能を再実装する必要があります。この新しい OO メソッドを人々に使ってもらうことが、ユーザビリティに対する最大の要因です。言い換えれば、再利用のためにさらに別の方法を作成することで、実際には物事を再利用しにくくすることになります。
はい。簡単に実現できます。関数ポインターの配列を使用し、それらの関数ポインターを使用して呼び出しを行います。関数を「オーバーライド」したい場合は、対応するスロットを新しい関数を指すように設定するだけです。これはまさに、C++ が仮想関数を実装する方法です。
glib や gtk 全体を例として追加していないことに少し驚いています。確認してください: http://www.gtk.org/features.php
2021 年 7 月 7 日現在の作業リンク: https://developer.gnome.org/glib/2.26/
gtk を使用するためにはかなりの定型コードが必要であり、最初から正しく理解するのはそれほど「簡単」ではないことは承知しています。しかし、それを使用したことがある場合、それは驚くべきことです。覚えておく必要がある唯一のことは、関数の最初のパラメーターとして一種の「オブジェクト」を使用することです。しかし、API を調べると、どこでも使用されていることがわかります。OOPで発生する可能性のあるメリットと問題について本当に良い例を作るIMHO-