なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

C言語でオブジェクト指向プログラミング

  • 2021/07/04
    • 載せたコードにミスがあったので修正
    • 「サブクラス」、「スーパークラス」を「派生クラス」、「基底クラス」に統一

前にRustはオブジェクト指向型言語ではないという記事を書いた後で、Twitterで以下のような発言をしていた。

今回はこれを有言実行しようと思う。

Cでオブジェクト指向プログラミング(以下OOP)をやっている例はほかにも存在しているのだが(これとかあれとか)、自分はあえてこれ以外のアプローチでOOPを実現する。

構造体を定義する

まずは構造体を定義する。ここまではほかの実現方法とほぼ一緒。

typedef struct {
    char name[256];
} object_t;

ほかのやり方では次にメソッドを定義するのだが、次に定義するのはメソッドではない。クラスである

クラスを定義する

今回は関数をクラスに見立てることで、Alan Kay氏のオブジェクト指向におけるメッセージングにより近い表現でOOPを実現する

void *Object(object_t *this, const char *msg, void *ret, va_list dap,
             ...)
{
    /* メソッドはここで定義される */
}

もちろん、継承などもこれをもとに実現する。というか、今回の方法のほうが実は継承を実現しやすい(当社比)。実現方法については後述する。

ではそれぞれの意味について解説する。
まずvoid *についてだが、これは汎用ポインタと呼ばれるものである。この型の変数は、いろいろな型のメモリアドレスを格納することができる優れものである。ただし汎用ポインタはあくまでvoid *型であるため、特定の型の値を読み書きするためには型キャストが必要である。以下に例を示す。

#include <stdio.h>

int main(void)
{
    /* 型変換が必要な例 (1) */
    {
        int var = 123;
        void *gp_ptr = &var;
        
        /* int型の値として読むためにint *型への変換が必要 */
        printf("*(int *)gp_ptr == %d\n", *(int *)gp_ptr);
    }
    
    /* 型変換が必要な例 (2) */
    {
        int var = 123;
        void *gp_ptr = &var;
        
        /* int型の値を書くためにint *型への変換が必要 */
        *(int *)gp_ptr = 456;
        
        printf("           var == %d\n", var);
    }
    
    return 0;
}
*(int *)gp_ptr == 123
           var == 456

次に引数の一番最後は...となっているが、これは省略したのではなく、立派なCの構文の一つである可変長引数と呼ばれるもので、これにより変数をいくらでも引き渡すことが可能となる
可変長引数を取り出すためにstdarg.hに含まれるva_list型とそれを使うためのマクロが必要である。以下に例を示す。

#include <stdio.h>
#include <stdarg.h>

void va_example(int arg_1, ...)
{
    /* va_list型変数の定義 */
    va_list ap;
    
    /* 可変長引数の開始(arg_1の次から開始される) */
    va_start(ap, arg_1);
    
    /* char *型の引数として取り出す */
    printf("%s\n", va_arg(ap, char *));
    
    /* 可変長引数の終了 */
    va_end(ap);
}

int main(void)
{
    va_example(0, "Hello.");
    
    return 0;
}
Hello.

以上をもとに、関数の型と引数を見てみよう。

void *Object(object_t *this, const char *msg, void *ret, va_list dap,
             ...)

これについてまとめた一覧表を以下に示す。

Object
引数説明
thisobject_t *インスタンス。ポインタである理由は参照渡しをして引き渡した変数自体を変更する必要があるため。
msgconst char *メッセージ。対応したメソッドを実行するために使う。
retvoid *戻り値。必要ない場合はNULLでも良い。クラス内で宣言したポインタを返さないようにする*1ためにその代替策として導入。
dapva_list派生クラスから基底クラスの引数付きメソッドを使うために必要。使わない場合はNULLを渡す。
戻り値void *あらゆるメソッドに適応した型を返すため汎用ポインタとしている。エラーはNULL

クラスにメソッド+αを実装する

ではクラスにメソッドとその他必要なものを実装しようと思う。

#include <stdio.h>
#include <stdarg.h>
#include <string.h>

/* 構造体定義部分は省略 */

/* クラスの定義 */
void *Object(object_t *this, const char *msg, void *ret, va_list dap,
             ...)
{
    void *ret_code = this;   /* デフォルトではインスタンス自身を返す */
    va_list ap;
    
    if (dap == NULL) {
        /* 可変長引数から取り出し開始 */
        va_start(ap, dap);
    }
    else {
        /* 派生クラスの可変長引数を使う場合コピーする */
        va_copy(ap, dap);
    }
    
    if (strcmp("new", msg) == 0) {
        /* コンストラクタ */
        char *name = va_arg(ap, char *);
        
        strcpy(this->name, name);
    }
    else if (strcmp("hello", msg) == 0) {
        printf("Hello, %s!\n", this->name);
    }
    else if(strcmp("name", msg) == 0) {
        /* ゲッターメソッド。メンバ変数があるなら入れたほうが良い */
        ret_code = &this->name;
    }
    else {
        /* エラー */
        ret_code = NULL;
    }
    
    /* 可変長引数の取り出し終了 */
    va_end(ap);
    
    return ret_code;
}

人によっては眩暈を起こすかもしれないが、順番に解説していこう。
まず、可変長変数の取り出し開始の動作は以下の2つに分岐されている。

    if (dap == NULL) {
        /* 可変長引数から取り出し開始 */
        va_start(ap, dap);
    }
    else {
        /* 派生クラスの可変長引数を使う場合コピーする */
        va_copy(ap, dap);
    }

*がついていないため違和感を覚えるかもしれないがva_list型はポインタである。そのため、NULLを代入しても良い。故に、NULLでの判定が可能である。そもそも、この引数dapは何に使うんだよ、と思われるかもしれないが、これは重要である。必要性については後述する。

この後に書かれているのは、皆さんお待ちかね(?)メソッドである。

    if (strcmp("new", msg) == 0) {
        /* コンストラクタ */
        char *name = va_arg(ap, char *);
        
        strcpy(this->name, name);
    }
    else if (strcmp("hello", msg) == 0) {
        printf("Hello, %s!\n", this->name);
    }

Cで書いたことがあるならわかると思うが、Cは文字列の扱いが苦手である。よって、文字列を比較するためにはstrcmp関数が必要となる。この文字列比較によって文字列が等価だった場合に処理を実行することで、メソッドを実現することができる
え、関数じゃないからメソッドじゃないって?メソッドが関数じゃなければならないと、誰が決めたのだろうか?メソッドはあくまでオブジェクトを操作する”手段”にすぎず、その実現方法が関数である必要はない。

ちなみに対応するメソッドがなかった場合、クラスはNULLを返すようになっている。

    else {
        /* エラー */
        ret_code = NULL;
    }

次に見てほしいのがゲッターメソッドである。これは継承を実現する際にあったほうが便利である。理由はいずれわかるだろう。

    else if(strcmp("name", msg) == 0) {
         /* ゲッターメソッド。メンバ変数があるなら入れたほうが良い */
        ret_code = &this->name;
    }

これで粗方クラスとメソッドの書き方はわかったのではないだろうか。ではこれを使った例を以下に示す。

#include <stdio.h>
#include <stdarg.h>
#include <string.h>

/* 構造体, クラス定義は省略 */

int main(void)
{
    /* インスタンスの生成 */
    object_t ins_1;
    
    /* インスタンスの初期化 */
    /* ins_1.nameに"C"を代入している */
    Object(&ins_1, "new", NULL, NULL, "C");
    
    /* helloメッセージを送る */
    /* "Hello, C!"が標準出力されるメソッドが実行される */
    Object(&ins_1, "hello", NULL, NULL);
    
    /* メッセージに対応したメソッドがない場合、エラーになる */
    if (Object(&ins_1, "abcd", NULL, NULL) == NULL) {
        printf("Class does not have \"abcd\" method.\n");
    }
    
    return 0;
}
Hello, C!
Class does not have "abcd" method.

継承

では次に継承を実現してみる。面倒なので一気に見せる。

/**
 * 継承
 */

/* object2_tの定義 */
typedef struct {
    object_t super;
    char derive_name[256];
} object2_t;

/* super関数(基底クラスを使うための関数) */
void *object2_super(object2_t *this, const char *msg, void *ret,
                    va_list ap)
{
    return Object(&this->super, msg, ret, ap);
}

/* Object2クラスの定義 */
void *Object2(object2_t *this, const char *msg, void *ret, va_list dap,
              ...)
{
    void *ret_code = this;
    va_list ap;
    
    if (dap == NULL) {
        /* 可変長引数から取り出し開始 */
        va_start(ap, dap);
    }
    else {
        /* 派生クラスの可変長引数を使う場合コピーする */
        va_copy(ap, dap);
    }
    
    if (strcmp("new", msg) == 0) {
        /* コンストラクタ */
        char *derive_name = va_arg(ap, char *);
        
        object2_super(this, msg, NULL, ap);
        strcpy(this->derive_name, derive_name);
    }
    else if(strcmp("derive_name", msg) == 0) {
        /* ゲッターメソッド。メンバ変数があるなら入れたほうが良い */
        ret_code = &this->derive_name;
    }
    else {
        /* 基底クラスを使う */
        ret_code = object2_super(this, msg, ret, ap);
    }
    
    /* 可変長引数の取り出し終了 */
    va_end(ap);
    
    return ret_code;
}

ということで、ここから必要なところを取り出していく。

まずは構造体の定義。メンバ変数も含めて継承するためsuperを定義している。

/* object2_tの定義 */
typedef struct {
    object_t super;
    char derive_name[256];
} object2_t;

次に、super関数であるobject2_superを定義する。基底クラスを使うための関数である。ただし、必要ないため可変長引数はない。

/* super関数(基底クラスを使うための関数) */
void *object2_super(object2_t *this, const char *msg, void *ret,
                    va_list ap)
{
    return Object(&this->super, msg, ret, ap);
}

そして最後に、Object2クラスを見てほしい。特に注目してほしいのはこの2箇所である。

    if (strcmp("new", msg) == 0) {
        /* コンストラクタ */
        char *derive_name = va_arg(ap, char *);
        
        object2_super(this, msg, NULL, ap);
        strcpy(this->derive_name, derive_name);
    }
    else {
        /* 基底クラスを使う */
        ret_code = object2_super(this, msg, ret, ap);
    }

コンストラクタと対応したメソッドがない場合に先ほど定義したobject2_superが呼び出されている。
ここでコンストラクタではObject2va_listが渡されているされていることに気づいたのではないだろうか。
この渡したva_listは最終的にはここで使われる。

/* クラスの定義 */
void *Object(object_t *this, const char *msg, void *ret, va_list dap,
             ...)
{
    void *ret_code = this;   /* デフォルトではインスタンス自身を返す */
    va_list ap;
    
    if (dap == NULL) {
        /* 可変長引数から取り出し開始 */
        va_start(ap, dap);
    }
    else {
        /* 派生クラスの可変長引数を使う場合コピーする */
        va_copy(ap, dap);
    }
    
    /* 省略 */
    
}

そう、Objectdap引数に渡され、va_copyマクロ*2によってコピーされる。したがって、派生クラスの可変長引数を基底クラスに渡すことが可能となる。これがdap引数の用途である。

対応したメソッドがない場合に使われるsuper関数については、以下のコードを見たほうが早い。

#include <stdio.h>
#include <stdarg.h>
#include <string.h>

/* 構造体, クラス定義は省略 */

int main(void)
{
    /* インスタンスの生成 */
    object2_t ins_2;
    
    /* インスタンスの初期化 */
    /* ins_2.nameに"C"を、ins_2.derive_nameに"C++"を代入している */
    Object2(&ins_2, "new", NULL, NULL, "C++", "C");
    
    /* helloメッセージを送る */
    /* Object2クラスに"hello"メソッドはなかったはずだが・・・? */
    Object2(&ins_2, "hello", NULL, NULL);
    
    return 0;
}
Hello, C!

もうわかったのではないだろうか。対応するメソッドがなかった場合、基底クラスからメソッドを探し、見つけたら実行している。つまり以下のようになっている。

f:id:opaupafz2:20210703210526p:plain
基底クラスのメソッド呼び出し

以上から、継承を実現できていることがわかる。
そしてここからあることに気づいた人もいるだろう。派生クラスに基底クラスにもあったメソッドを追加してあげれば、なんとメソッドのオーバーライドも実現できる

それを確かめるためにまた継承してみよう。

/**
 * 継承(2)
 */

/* object3_tの定義 */
typedef struct {
    object2_t super;
} object3_t;

/* object3_super関数は省略 */

/* Object3クラスの定義 */
void *Object3(object3_t *this, const char *msg, void *ret, va_list dap,
              ...)
{
    void *ret_code = this;
    va_list ap;
    
    if (dap == NULL) {
        /* 可変長引数から取り出し開始 */
        va_start(ap, dap);
    }
    else {
        /* 派生クラスの可変長引数を使う場合コピーする */
        va_copy(ap, dap);
    }
    
    if (strcmp("new", msg) == 0) {
        /* コンストラクタ */
        object3_super(this, msg, NULL, ap);
    }
    else if (strcmp("hello", msg) == 0) {
        /* メソッドのオーバーライド */
        printf("Hello, %s and %s!\n",
               (char *)object3_super(this, "name", NULL, NULL),
               (char *)object3_super(this, "derive_name", NULL, NULL));
    }
    else {
        /* 基底クラスを使う */
        ret_code = object3_super(this, msg, ret, ap);
    }
    
    /* 可変長引数の取り出し終了 */
    va_end(ap);
    
    return ret_code;
}

Object3クラスにObjectクラスにもあったhelloメソッドを追加した。

    else if (strcmp("hello", msg) == 0) {
        /* メソッドのオーバーライド */
        printf("Hello, %s and %s!\n",
               (char *)object3_super(this, "name", NULL, NULL),
               (char *)object3_super(this, "derive_name", NULL, NULL));
    }

これを使ってみる。

#include <stdio.h>
#include <stdarg.h>
#include <string.h>

/* 構造体, クラス定義は省略 */

int main(void)
{
    /* インスタンスの生成 */
    object_t ins_1;
    object3_t ins_3;
    
    /* インスタンスの初期化(Object) */
    /* ins_1.nameに"C"を代入している */
    Object(&ins_1, "new", NULL, NULL, "C");
    
    /* インスタンスの初期化(Object3) */
    /* ins_3.nameに"C"を、ins_3.derive_nameに"C++"を代入している */
    Object3(&ins_3, "new", NULL, NULL, "C++", "C");
    
    /* helloメッセージを送る(Object) */
    Object(&ins_1, "hello", NULL, NULL);
    
    /* helloメッセージを送る(Object3) */
    Object3(&ins_3, "hello", NULL, NULL);
    
    return 0;
}
Hello, C!
Hello, C and C++!

これでメソッドのオーバーライドも実現できることを確認できたのではないだろうか。

メンバ変数の継承について

実はこの継承方法、一つだけ問題点がある。それは、継承が深くなるごとに参照するメンバ変数も深くなることである

instance.super.super.super.super./* ずっと続くよ! */.super.member;

深くなった場合、このようにしなければならない。当然だが、いちいちアクセスするのは非常に面倒である。
そこで、ゲッターメソッドの出番である。ゲッターメソッドがあれば、どれだけ継承が深くなっても、以下の記述だけで済む

/* 変数の場合 */
*(T *)Class(&instance, "member", NULL, NULL);
/* ポインタの場合 */
(T *)Class(&instance, "member", NULL, NULL);

ちなみに先ほどObject3メソッドにてhelloメソッドをオーバーライドしたが、ここでもゲッターメソッドは活躍している。

    else if (strcmp("hello", msg) == 0) {
        /* メソッドのオーバーライド */
        printf("Hello, %s and %s!\n",
               (char *)object3_super(this, "name", NULL, NULL),
               (char *)object3_super(this, "derive_name", NULL, NULL));
    }

これをゲッターメソッドなしで実装しようとすると、こうなる。

    else if (strcmp("hello", msg) == 0) {
        /* メソッドのオーバーライド */
        printf("Hello, %s and %s!\n", this->super.super.name,
               this->super.derive_name);
    }

この時点ではこちらのほうがきれいに見えるかもしれないが、継承が深くなるとこれどころではなくなるだろう。
そしてメンバ変数がどの構造体のメンバ変数なのか探る必要がないのもゲッターメソッドの利点と言える。故に、ゲッターメソッドはあったほうが良い。

終わりに

CでOOPについては以上である。まだ改善の余地はあると思われるが、Cでもオブジェクト指向プログラミングができなくもないことがわかったのではないだろうか。ここから、オブジェクト指向プログラミングができることは必ずしもオブジェクト指向型言語であることとイコールではないことがわかる。

さらに、たとえ様々な手法のプログラミングができてもプログラミング言語にそのパラダイムを持っていると断言するのは難しいとも言えるのではないだろうか。
もしかしたら「自分はこう断言しているけど、実はそのパラダイムを持っていなかった」ということもあるかもしれない。
この記事に共感できたという方はこれを機に「自分はこう思っていたけど、本当にそうなのか?」と疑問を持ってみてはいかがだろうか。

*1:返してしまうと、意図しない動作を引き起こす原因となってしまう

*2:ちなみにva_copyマクロはC99以降でしか使えないので注意が必要である