問.Cでオブジェクト指向プログラミングを行なえ

問.Cでオブジェクト指向プログラミングを行なえ。ただし「オブジェクト指向プログラミング」とは、次のような特徴を持つプログラミング技法であるものとする:

  1. オブジェクトの実装はオブジェクトのユーザーからは隠蔽される(カプセル化/隠蔽)
  2. 同一型のオブジェクトと同一メソッドを与えた時、実際のメソッドの動作はオブジェクトの内容により変化する(ポリモーフィズム多態性

なお、ユーザーが既存のオブジェクトをカスタマイズして新たなオブジェクトを作成する機能は、必要ないものとする。

この問いの狙い

よく、「オブジェクト指向プログラミング」と「オブジェクト指向言語」は混同されます。が、前者はプログラムを設計する上での考え方で、後者はその考え方を容易にソースコードに書けるような仕様になっている言語の事で、全く違うものを指しています。
その証拠を示すため、「非オブジェクト指向言語」たるC言語で「オブジェクト指向プログラミング」をしてみよう、というのが今回の狙いですね。よくあるネタですのでわざわざ当ブログで取り上げるまでもないかもしれませんが、弊社内にもわかっていない人がいたのでメモ代わりに。

先に述べておきますと、Windows API が実は、実際にC言語オブジェクト指向プログラミングを行なった実例の一つになっています。試しに下記のソースコードHWNDを使ってウィンドウを操作するプログラムを比較してみると、よくおわかりになるでしょう(実際のHWNDはセキュリティ的な問題((かつて、上手くデータを作ってそのアドレスをHWNDとして渡すと、Windows カーネルの権限で任意のコードが実行できるというセキュリティホールがあったようです))を回避するため、単純なポインタではありませんが)。

Cでカプセル化

……などと大仰な章題をつけましたが、C言語が使えると自称するのであれば当然使えて欲しいテクニック。
ヘッダーファイルでは構造体をポインタとしてのみ宣言し、ソースファイルで具体的な構造を定義、全ての構造体操作を専用の関数で行なう手法です。

/* oopforc.h */
#ifndef OOP_FOR_C_H__INCLUDED
#define OOP_FOR_C_H__INCLUDED

typedef struct tagObject *Object;

Object CreateObject(int hoge);
void DeleteObject(Object obj);
int GetHoge(Object obj);
void SetHoge(Object obj, int newhoge);
void PrintObject(Object obj);

#endif /* OOP_FOR_C_H__INCLUDED */
/* oopforc.c */
#include "oopforc.h"
#include <stddef.h>
#include <stdio.h>

struct tagObject
{
    int m_hoge;
};

Object CreateObject(int hoge)
{
    Object obj = malloc(sizeof(struct tagObject));
    if (!obj) return NULL;
    obj->m_hoge = hoge;
    return obj;
}
void DeleteObject(Object obj)
{
    free(obj);
}
int GetHoge(Object obj)
{
    return obj->m_hoge;
}
void SetHoge(Object obj, int newhoge)
{
    obj->m_hoge = newhoge;
}
void PrintObject(Object obj)
{
    printf("PrintObject: %d\n", obj->m_hoge);
}

使用法としてはこうなります:

/* main.c */
#include "oopforc.h"
#include <stdio.h>

int main(void)
{
    Object obj = NULL;

    obj = CreateObject(5);
    if (!obj) return 1;

    PrintObject(obj);    // PrintObject: 5
    printf("printf: %d\n", GetHoge(obj)); // printf: 5

    SetHoge(obj, 10);

    PrintObject(obj);    // PrintObject: 10
    printf("printf: %d\n", GetHoge(obj)); // printf: 10

    DeleteObject(obj);

    return 0;
}

実際は、各種の関数内で「与えられたobjが有効かどうか」をチェックする方が安全ですが、その辺りは今回は割愛します。

Cでポリモーフィズム

今回の課題は「既存クラスを継承したクラスをユーザー定義できる必要はない」という事にしましたので、仮想関数を使ったC++風のポリモーフィズムまでは不要でしょう。大抵はコールバック関数で事足りるはずです。
ですので、struct tagObjectに型タイプを示す列挙体を作り、実行時型を共用体に入れておけば十分にポリモーフィズムが実現できるでしょう。

……と言うと「あれっ?」と思う方もいらっしゃると思うのでちょっと補足しておきます。
ふだん「ポリモーフィズム」と言うと継承を思い浮かべがちですが、同じ書き方でオブジェクトに応じて動作さえ変わればとりあえずはポリモーフィズム。具体的な実装が継承なのかダックタイピングなのかテンプレートなのか、あるいは原始的なswitchによる分岐なのかは、オブジェクトのユーザーにとってはさっぱり関係のない話です。

/* oopforc.h */
#ifndef OBJECT_H__INCLUDED
#define OBJECT_H__INCLUDED

typedef struct tagObject *Object;

Object CreateIntObject(int value);
Object CreateStrObject(const char *value);
void DeleteObject(Object obj);
void PrintObject(Object obj);

#endif /* OBJECT_H__INCLUDED */
/* oopforc.c */
#include "oopforc.h"
#include "private/IntObject.h"
#include "private/StrObject.h"
#include <stddef.h>
#include <stdlib.h>

static enum EObjectType
{
    OBJ_INT,
    OBJ_STR,
};

struct tagObject
{
    enum EObjectType m_type;
    union
    {
        struct tagIntObject m_intobj;
        struct tagStrObject m_strobj;
    };
};

Object CreateIntObject(int value)
{
    Object obj = malloc(sizeof(struct tagObject));
    if (!obj) return NULL;

    obj->m_type = OBJ_INT;
    if (!InitializeIntObject(&obj->m_intobj, value))
    {
        free(obj);
        return NULL;
    }
    return obj;
}
Object CreateStrObject(const char *value)
{
    Object obj = malloc(sizeof(struct tagObject));
    if (!obj) return NULL;

    obj->m_type = OBJ_STR;
    if (!InitializeStrObject(&obj->m_strobj, value))
    {
        free(obj);
        return NULL;
    }
    return obj;
}

void DeleteObject(Object obj)
{
    if (!obj) return;

    switch (obj->m_type)
    {
    case OBJ_INT:
        TerminateIntObject(&obj->m_intobj);
        break;
    case OBJ_STR:
        TerminateStrObject(&obj->m_strobj);
        break;
    default:
        return;
    }

    free(obj);
}

void PrintObject(Object obj)
{
    switch (obj->m_type)
    {
    case OBJ_INT:
        PrintIntObject(&obj->m_intobj);
        break;
    case OBJ_STR:
        PrintStrObject(&obj->m_strobj);
        break;
    default:
        return;
    }
}
/* main.c */
#include "OOPforC.h"

int main(void)
{
    Object intobj = NULL, strobj = NULL;

    intobj = CreateIntObject(5);
    if (!intobj) return 1;
    
    strobj = CreateStrObject("hoge");
    if (!strobj) {
        DeleteObject(intobj);
        return 2;
    }

    PrintObject(intobj);
    PrintObject(strobj);

    DeleteObject(intobj);
    DeleteObject(strobj);

    return 0;
}

後は private/XXXObject.h 、private/XXXObject.c をご自由にご実装下さい。お好みで、CreateXXXObject()で確保するメモリサイズを引数に応じて変更できるように(struct tagStrObject{ char m_str[0]; };みたいな形式になってる場合)。

余談

こいつを真面目に「Cでもユーザー定義の継承とか動的型情報の取得とかできるようにしたいんだー!」とか言い出しちゃった人達が某 Micr○soft とかいう会社にいて、結果として生まれたのが COM なる技術でした。
もっとも、GUID をレジストリに登録しておけば自動的に DLL を探し出して関数定義を動的リンクできるよねー、的な発想まで加わっているせいで、初見では何やってるのかさっぱりわからないものに見えますが。