C言語でOpaqueポインタを使って構造体のメンバを隠蔽する


このエントリーをはてなブックマークに追加

C言語で、自作の構造体のメンバをユーザに開示しないテクニックとして、Opaqueポインタというものが知られています。今回は、書籍「C++のためのAPIデザイン」の3.1.6節を参考に、Opaqueポインタを使う簡単なサンプルを紹介します。

Opaqueポインタを使わない場合

人に関するデータを集めた構造体Personと、その構造体を使ったライブラリを作成することを考えます。以下にPerson.hのコードを示します。

#pragma once

typedef struct _Person
{
    int age;
} Person;

// 以下、Person構造体に関するAPI

Person* createPerson(int age);    // Person構造体のオブジェクトを生成
void printPerson(Person* ptr);    // Person構造体を使った操作(メンバのプリント)
void destroyPerson(Person* ptr);  // Person構造体のオブジェクトを破棄

Person構造体のメンバがベタ書きされていることに注目してください。

Person.cppのコードを以下に示します。

#include "Person.h"
#include <stdio.h>

Person* createPerson(int age)
{
    Person* ptr = (Person*)malloc(sizeof(Person));
    if (ptr)
    {
        ptr->age = age;
    }
    return ptr;
}

void printPerson(Person* ptr)
{
    if (ptr) printf("age: %d\n", ptr->age);
}

void destroyPerson(Person* ptr)
{
    if (ptr) free(ptr);
}

と記述します。

このPerson構造体の呼び出し側のコードは以下のとおりです。

#include "Person.h"

int main(void)
{
    Person *p = createPerson(42); // オブジェクトを生成
    printPerson(p);
    destroyPerson(p); // オブジェクトを破棄

    return 0;
}

createPerson()を使ってPerson構造体のオブジェクトを新規作成し、Person構造体を利用する関数を呼び出した後、destroyPerson()を使ってオブジェクトを破棄しています。

この例ではPerson.hにPerson構造体のメンバをすべてベタ書きしているため、以下のような問題が生じます。

Personオブジェクトの中身を、呼び出し側が自由に変更できてしまう。 上記例では、呼び出し側がp->age = 100;などと内部の値を自由に変更できてしまいます。これはライブラリの作者が予期しない結果をもたらす可能性があります。

Person構造体の実装が外部に露出してしまう。 Person構造体の実装は呼び出し側が知る必要がない情報であっても、実装が露出してしまっています。

Person構造体に変化があるたびに、Person.hの呼び出し側で再コンパイル・再リンクが必要。 ビルドの時間が増加します。またライブラリ側と呼び出し側で異なる定義のPerson構造体を想定している場合、リンクすると危険なことが起こるはずです。

Opaqueポインタを使う場合

上述の問題を解決するためにOpaqueポインタが使われます。修正したPerson.hを以下に示します。

#pragma once

typedef struct Person *PersonPtr;

PersonPtr createPerson(int age);
void printPerson(PersonPtr ptr);
void destroyPerson(PersonPtr ptr);

上記では、Person構造体の定義はしていません。すなわち、Person構造体のメンバを具体的に書き下すことはしていません。そのかわりに、どこかに定義されているPerson構造体へのポインタを、PersonPtrという型名で呼ぶという約束だけをしています。こうすることでPerson.hの呼び出し側にPerson構造体の実装が漏れることがなくなりました。

修正したPerson.cppは以下です。

#include "Person.h"
#include <stdio.h>
#include <stdlib.h>

struct Person
{
    int age;
};

PersonPtr createPerson(int age)
{
    PersonPtr ptr = (PersonPtr) malloc(sizeof(struct Person));
    if (ptr)
    {
        ptr->age = age;
    }
    return ptr;
}

void printPerson(PersonPtr ptr)
{
    if (ptr) printf("age: %d\n", ptr->age);
}

void destroyPerson(PersonPtr ptr)
{
    if (ptr) free(ptr);
}

呼び出し側は以下です。

#include "Person.h"

int main(void)
{
    PersonPtr p = createPerson(42);
    printPerson(p);
    destroyPerson(p);

    return 0;
}

構造体の中身は呼び出し側からは知る由はないので、もし仮にここでp->age = 70;などと構造体の中身を書き換えようとしても、コンパイルが通りません。Opaqueポインタを使うことで実装の詳細を適切に隠蔽でき、安全性も向上していることがわかります。