[C]オペークポインタ

2016年8月22日月曜日

C

「いくつかの言語でフィボナッチ数生成」を考える。ではハンドルに配列そのものを使っていたけど、CのAPIでハンドルを使う場合はオペークポインタにすることが多いよ。

「いくつかの言語でフィボナッチ数生成」を考える。の結果をオペークポインタを使ったものに変えてみよう。
まずは呼び出し側を見てみよう。

main.c
#include <stdio.h>
#include "fib.h"

int main(void) {
    hSequence fib = fibonacci_new();

    for (int i = 0; i < 10; i++) {
        printf("%d\n", fibonacci_next(fib));
    }

    fibonacci_delete(fib);

    return 0;
}
fib.h
#include "sequence.h"

hSequence fibonacci_new(void);
void fibonacci_delete(hSequence handle);
int fibonacci_next(hSequence handle);
sequence.h
typedef struct Sequence *hSequence;

sequence.hに唐突に typedef struct Sequence *hSequence; って出てくるね。struct Sequenceの中身が定義されていないのに!

これは不完全型っていう型の種類だよ。
不完全型はJavaなんかでは出てこないのでわかりにくいかもしれないけど、構造体や配列の宣言時に構造体のメンバーや配列の長さを伴わずに宣言するやりかただよ。

コンパイル時にオブジェクトサイズを決定するんだけど、Cではコンパイル時の前方参照ができないから、定義中にお互いが出てくるようなデータ構造だと困っちゃうんだね。 そういう時は不完全型を先に宣言しておくよ。

不完全型はこの解説がわかりやすい。
Sun Studio 12: C ユーザーズガイド
6.11 不完全な型
https://docs.oracle.com/cd/E19205-01/820-1209/bjals/

で、この不完全型を使ってオペークポインタを作るよ。
オペークポインタというのはopaque(不透明)なポインタっていう意味で、ポインタのさしている中身を知らなくてもポインタだけ使えればいいっていう場面に使う。
不完全型は構造体に中身を知らなくても宣言できるので、この役割にぴったりだね。

今度は呼び出される側を見てみよう。

seq.h
#include "sequence.h"

struct Sequence {
    size_t size;
    int    term[];
};

int sequence_next(hSequence handle, int f(int*));
sequence.c
#include <string.h>
#include "seq.h"

static int sequence_next0(int term[], size_t n, int f(int*));
static int shift_intarr(int arr[], size_t n);

int sequence_next(hSequence handle, int f(int*)) {
    return sequence_next0(handle->term, handle->size, f);
}

static int sequence_next0(int term[], size_t n, int f(int*)) {
    int next = f(term);
    int s = shift_intarr(term, n);

    term[n - 1] = next;

    return s;
}

static int shift_intarr(int arr[], size_t n) {
    int s = arr[0];

    memmove(arr, arr + 1, sizeof(int) * (n - 1));

    return s;
}
fibonacci.c
#include <stdlib.h>
#include "seq.h"

static int fibonacci(int term[]);

hSequence fibonacci_new(void) {
    hSequence handle = malloc(sizeof(struct Sequence) + sizeof(int) * 2);
    handle->size = 2;
    handle->term[0] = 1;
    handle->term[1] = 1;

    return handle;
}

void fibonacci_delete(hSequence handle) {
    free(handle);
}

int fibonacci_next(hSequence handle) {
    return sequence_next(handle, fibonacci);
}

static int fibonacci(int term[]) {
    return term[0] + term[1];
}

Sequence構造体のメンバー定義はAPI公開用のsequence.hには登場せずにseq.hだけに登場している。
これであれば、Sequence構造体のメンバーを変更しなければいけないような事態が生じても呼び出し側に影響を及ぼすことなく、変更することができる。

これはまさに標準ライブラリの入出力関数のデザインであるし、状態を持ったハンドルと一連の操作関数群で実際の操作を隠すという考え方はオブジェクト指向だね。

参考:
DCL12-C. 抽象データ型は opaque な型を使って実装する
https://www.jpcert.or.jp/sc-rules/c-dcl12-c.html