コードパターン: 構造体に埋め込まれた関数ポインタ¶
はじめに¶
開発者は同じ名前の操作関数の呼び出しで、データ毎に処理を変えるという要求を持ちます。この要求はあまり一般的なので、新しい言語では文法レベルでそのような要求に応えています。オブジェクト指向言語で言うところのポリモフィズムです。C言語にはそういった仕掛けは無いので開発者が独自にそのような仕掛けを作る必要があります。しかし書き方はおおまかには決まっていて、それをコードパターンととらえて読解に役立てることができます。
同じ型のデータに対して操作関数群が定義されている、というコードパターン(コードパターン: データ型にひも付く関数群) と関数ポインタとコールバック関数(コードパターン: 関数ポインタとコールバック) を組合せて実現することが多いです。
パターンに含まれる断片の役割¶
このコードパターンには(重複する箇所はありますが)3つの断片からなると考えることができます。
クライアントコード
データの種類をできるだけ気にすることなく、データを操作したいという立場にあるコードです。「仕掛け」の提供する外向けのインターフェイスを利用します。
データの種類特有のコード(バックエンド)
「仕掛け」の提供する内向きのインターフェイスに適合する形式でデータの種類に応じた操作を実現します。(pattern1. 緑の部分) 文脈によってはプラグインを呼んだりすることもあります。
仕掛けを提供するコード
3.1 バックエンドの実装を既定する型 (内向きのインターフェイス)
(pattern3. 青の部分) 仕掛けを作る側がバックエンドに期待する実装形式を示します。
3.2 クライアントコード向けの操作関数(外向きのインターフェイス)
(pattern2. 赤の部分) クライアントからのデータの初期化の依頼やデータ操作を受けつけます。内部で、内向きのインターフェイスを通して具体的な処理をバックエンドに依頼します。
例: 長さを保持するデータと印字操作¶
単純な例を示します。「長さ」を保持するデータ構造と、その操作として保持する長さを印字する関数を考えます。キロメートルの単位で距離を保持するデータとメートルの単位で距離を保持するデータの2種類がありますが、印字の操作にあたって、その種類を気にせず同じ関数で操作できます。
実際には「コードパターン」などと大袈裟なものを持ち出すこと無く実現できますが、ここでは説明のためにこの「コードパターン」で実装してみました。
#include <stdio.h>
#include <stdlib.h>
struct Length {
int L;
int (* print) (struct Length * l, FILE *fp);
};
struct Length * newLengthInKiloMeter (int km);
struct Length * newLengthInMeter (int m);
int print_length (struct Length * l, FILE *fp);
int
main(void)
{
struct Length *km = newLengthInKiloMeter ( 3 );
struct Length *m = newLengthInMeter ( 5 );
print_length(km, stdout);
print_length(m, stdout);
return 0;
}
int print_length
(struct Length * l, FILE *fp)
{
return l->print(l, fp);
}
static int km_printer(struct Length * l, FILE *fp)
{
return fprintf(fp, "%dm\n", (l->L * 1000));
}
static int m_printer(struct Length * l, FILE *fp)
{
return fprintf(fp, "%dm\n", l->L);
}
struct Length *
newLengthInKiloMeter (int km)
{
struct Length *p = malloc (sizeof (struct Length));
p->L = km;
p->print = km_printer;
return p;
}
struct Length *
newLengthInMeter (int m)
{
struct Length *p = malloc (sizeof (struct Length));
p->L = m;
p->print = m_printer;
return p;
}
断片に分解して見て行きましょう。
クライアントコード¶
int
main(void)
{
struct Length *km = newLengthInKiloMeter ( 3 );
struct Length *m = newLengthInMeter ( 5 );
print_length(km, stdout);
print_length(m, stdout);
return 0;
}
newLengthInKiloMeterでキロメートルの単位で距離を保持する種類のデータを作成しています。(3km)
newLengthInMeterでメートルの単位で距離を保持する種類のデータを作成しています。(5m)
この後それぞれを標準出力に印字していますが、ここでprint_lengthというデータの種類によらない操作関数を利用しています。
バックエンド¶
static int km_printer(struct Length * l, FILE *fp)
{
return fprintf(fp, "%dm\n", (l->L * 1000));
}
static int m_printer(struct Length * l, FILE *fp)
{
return fprintf(fp, "%dm\n", l->L);
}
km_printerは、キロメートルの単位で距離を保持する種類のデータ用の印字関数です。
m_printerは、メートルの単位で距離を保持する種類のデータ用の印字関数です。
内向けのインターフェイス¶
struct Length {
int L;
int (* print) (struct Length * l, FILE *fp);
};
Length構造体のうち、特に printフィールドの型の部分が内向けのインターフェイスのインターフェイスとなります。バックエンドとして実装された2つの関数は このprintフィールドの型に合致するよう実装されていました。
外向けのインターフェイス¶
int print_length
(struct Length * l, FILE *fp)
{
return l->print(l, fp);
}
struct Length *
newLengthInKiloMeter (int km)
{
struct Length *p = malloc (sizeof (struct Length));
p->L = km;
p->print = km_printer;
return p;
}
struct Length *
newLengthInMeter (int m)
{
struct Length *p = malloc (sizeof (struct Length));
p->L = m;
p->print = m_printer;
return p;
}
print_lengthは自分自身では具体的な印字処理をせず、関数ポインタとしてprintフィールドが指し示すコールバック関数の処理を依頼しています。
newLengthInKiloMeterとnewLengthInMeterはそれぞれデータの種類毎にprintフィールドに適切なバックエンドの提供する関数を設定しています。
多数の操作関数を持つデータに対するより効率の良い実装¶
「長さ」の例は操作関数が1つ印字だけの単純なものでした。操作の数が増えたとき、操作毎に関数ポインタを保持するためのフィールドを追加していくと、データが大きくなってしまいます。
struct Length {
int L;
int (* print) (struct Length * l, FILE *fp);
int (* read) (struct Length * l, FILE *fp);
struct Length * (* copy) (struct Length * oirginal);
...
};
こういった場合、関数ポインタのフィールドだけを集めて独立したデータ構造((仮想)関数テーブル)を用意して、元のデータからはその関数テーブルを指し示すようにしているのを良く目にします。
struct Length {
int L;
struct LengthFunctions *ftable;
}
struct LengthFunctions {
int (* print) (struct Length * l, FILE *fp);
int (* read) (struct Length * l, FILE *fp);
struct Length * (* copy) (struct Length * oirginal);
...
};