C言語で デザインパターンにトライ! Prototypeパターン ~原型クラスをクローンして利用しよう!~ Shallow CopyとDeep Copy


はじめに

「C言語でトライ! デザインパターン」
今回はPrototypeについてです。ポインタを利用するC言語にとって凝った構造体の生成/削除が面倒なので、この思想は大いに利用価値がありそうです。

デザインパターン一覧
作成したライブラリパッケージの説明
公開コードはこちら

Prototypeパターン

wikipediaの説明は以下となります。

生成されるオブジェクトの種別がプロトタイプ(典型)的なインスタンスであるときに使用され、このプロトタイプを複製して新しいオブジェクトを生成する

ベースとなるクラスから、新しい実体を中身もコピーした状態で作成(クローン)して使おうという考え。シンプルだけど色々な場所で利用できる考え方です。

例えばOpenCVという画像処理系のライブラリで利用するMat構造体は、この考えに従ってcloneして使うことが出来ます。簡単に言うとMatは1枚の画像情報を保持していて、cloneを使うことでサクッと画像をコピーすることができます。

gitのcloneもリポジトリ情報をそのままコピーしてくる形なので同じ考え方と言えそうですね。

Shallow CopyとDeep Copy

このパターンのように新しい実体にデータをコピーする場合、Shallow CopyとDeep Copyというコピー方法についてどちらを採用するか考える必要があります。

これらが何か。こちらのサイトの説明・イラストが非常にシンプルでわかりやすかったので、画像をお借りして説明します。

Shallow Copy

大本の実体は別ものになるのですが、その中で参照しているデータは変更せず同じものを参照させるコピー方法です。
C言語だと、ポインターを沢山持っている構造体をmemcpyするだけだとこのshallow copyの形になります。

コピーするものが少ないので実装も楽で処理も速い代わりに、同じものを参照するのでデータ管理が面倒になります。
誰が元ネタを管理するのかちゃんと考えないといけない。

Prototypeパターンの例で挙げたMat構造体のcloneはshallow copyです。JavaCV(OpenCVのJavaラッパー)でハマった記憶あり。
2018/08/31追記 shallow copyはMat構造体のcloneじゃなく、JavaCVの構造体変換APIであるconvertでした。再度確認したところ、Mat構造体のcloneはdeep copyです。

Deep Copy

こちらは参照しているデータの中身も別の実体を用意して、完全に別物となるようにするコピー方法です。

完全に元ネタと切り離されて利用する為データ管理がわかりやすいです。gitのcloneはこちらですね。

その反面、コピーするものが増えるので実装も増え処理も遅くなります。OpenCVは動画のエンコード等でも利用されているので、Deep Copyだと速度的にとてもじゃないけど使えないでしょうね。

Shallow CopyとDeep Copy どちらがいいの?

どちらも手段の1つでメリット・デメリットあるので、優劣をつけるようなものではないです。一部はDeep Copyにして、同じ情報だけShallow Copyにするなんてのもいいですしね。

なので逆に、何も考えずに楽だからとShallow Copyを使ったり、とにかく結合度を低くしないととDeep Copyしか使わなかったりというのも危ない気はします。
好みもあるとは思いますが、その時々で適切な手段を利用しましょう。

余談:このパターン、Flyweightパターンと相性いいのでは?

Shallow Copyのデメリットは誰が元ネタを管理するのかちゃんと考えないといけないという点でした。
一方、Flyweightパターンはデータ生成したか意識せずに同じインスタンスを使いまわそうぜ!という発想。インスタンスの生成・管理は1クラスにお任せ。

なんかこの2つ相性良さそうですね。例えばコピーの仕方をFlyweightを利用したインスタンス取得にすれば、そのインスタンスの管理は気にせず利用することが出来ます。

また、Flyweightは実装的にはインスタンスを生成するのと取得するのが同じAPIで実現するので、Deep Copyな実装だけど、実は一部Shallow Copyになっているから、両方のいいとこどりが出来たりしそう。
ぱっと思いついたのは例えばHTTPレスポンスを作成する処理の場合、ベースとなるレスポンスデータのデータ構造 + 固定のHTTPレスポンスヘッダーをprototypeとして登録。

  • レスポンスの内容は毎回違うのでDeep Copyで実現する。
  • でも実は固定のHTTPレスポンスヘッダーはFlyweightで取得している。

という形でcloneの実装をすれば、Deep Copyの安心感と一部Shallow Copyの速度が活かせる構造になりそう。

ライブラリ

概要

基本方針としてはこう。
1. 元データとclone方法をライブラリに登録
2. 登録情報を利用してclone。ライブラリ内で登録されたclone方法に従ってclone実施
3. 使い終わったら登録を削除

処理はまるっとユーザー任せな単純なライブラリにはなっています。
ただclone時に何をどうコピーすればいいか意識しなくていいので、ポインタ地獄な構造体の利用が凄い楽になります。

クラス設計

ライブラリにprototypeを登録するためのインターフェイスをPrototypeManagerInterfaceと定義し、その実装クラスであるPrototypeManagerのインスタンスをライブラリAPIを介して利用者に生成してもらう形をとりました。
ライブラリ内で隠ぺいしても良かったんですが、prototypeによってthreadsafeにしたいケースとしたくないケースが共存しそうだなと感じたので、それぞれのケースを同じアプリケーション内で利用できるようにと。後はprototype登録は結構な数になると、一々削除しなくてもまとめてがつんと消せるように。

prototypeを登録するとPrototypeFactoryが作成されるので、PrototypeFactoryを利用してprototypeをcloneします。

利用方法としてはこんな感じを考えています。
- データ生成を行う際には管理クラスを作ると思うので、そこにPrototypeManagerを持ってもらう。
- prototypeを登録するクラスにcloneの為の処理を実装してもらい、登録してもらう
- 出来たPrototypeFactoryをAPI等を介して他のクラスに利用してもらう。

一応デフォルトのclone APIとしてmemcpy/freeを用意してますが、これら全てをデフォルトで賄えるようなデータ構造なら、わざわざこのライブラリを使う理由がないかな。複雑なデータ、copyじゃ遅すぎるデータに対する改善のためのライブラリなので。

いい点

  • ポインタだらけの複雑な構造体コピーが"clone"だけで終わる
  • 削除はAPI一つなので楽

使いどころ

  • ポインタだらけのデータをやたらコピーする必要がある時
  • 一部shallow copyにして処理速度を速めたい時

欠点

  • 結局clone実装は実質ユーザー任せ

動作環境:

Ubuntu 18.04 Desktopで動作確認済み。大抵のLinux環境で動作すると思います。

詳細

API定義

prototype.h
/*PrototypeManager定義*/
struct prototype_manager_t;
typedef struct prototype_manager_t *PrototypeManager;

/*PrototypeFactory定義*/
struct prototype_factory_t;
typedef struct prototype_factory_t *PrototypeFactory;

/*ユーザーが実装する関数定義。デフォルトはmemcpy, free*/
struct prototype_factory_method_t {
        void * (*clone) (void * base, size_t base_length);
        void (*free) (void * cloned_data);
        void (*free_base) (void * base_data);
};
typedef struct prototype_factory_method_t prototype_factory_method_t;

/*PrototypeManagerのnew/free*/
PrototypeManager prototype_manager_new(int is_threadsafe);
void prototype_manager_free(PrototypeManager this);

/*prototypeの登録/削除*/
PrototypeFactory prototype_register(PrototypeManager this, void * base, size_t base_length, prototype_factory_method_t * factory_method);
void prototype_unregister(PrototypeManager this, PrototypeFactory factory);

/*データのclone/free*/
void * prototype_clone(PrototypeFactory this);
void prototype_free(PrototypeFactory this, void * cloned_data);

使い方:

  1. prototype_manager_newでPrototypeManagerを生成。
  2. prototype_registerでprototype登録。clone処理等は実装の上関数ポインタをfactory_methodに指定
  3. prototype_cloneで登録したprototypeのclone。prototype_freeで削除
  4. prototypeが用済みならprototype_unregisterで削除。終了時にまとめてprototype_manager_freeでもOK

コード

以下に置いてあります。
https://github.com/developer-kikikaikai/design_pattern_for_c/tree/master/prototype

サンプル

testコードを元に紹介。まずprototype_manager_new, prototype_registerで処理登録。test_msgdata_string_initはtest_msgdataのdata.stringをmallocして文字列コピーをしています。後はprototype_cloneでコピー。

test_msgdata_deep_copy, test_msgdata_deep_freeはdata.stringもmalloc/freeしているdeep copyになります。

main.c
typedef struct test_msgdata{
        int type;
        union {
                char * string;
                int * value;
        } data;
}test_msgdata;
...
static int test_prototype_multi_thread() {
         PrototypeManager manager = prototype_manager_new(0);
        if(test_prototype_base(manager)) {
                ERRORCASE
        }
...
}

static int test_prototype_base(PrototypeManager manager) {
        test_msgdata_string_init(&basedata_string, "deep_copy_base");
        prototype_factory_method_t deep_method ={test_msgdata_deep_copy, test_msgdata_deep_free, test_msgdata_deep_free};
        PrototypeFactory deep_string_factory = prototype_register(manager, basedata_string, sizeof(test_msgdata), &deep_method);

...

        /*clone shallow copy string*/
        {
        test_msgdata * clone_data = (test_msgdata *)prototype_clone(deep_string_factory);

今回登録したAPIはdeep copyなので、元データの内容が変更されても影響なしです。

main.c
        printf("deep copy value:%s, change base value\n", clone_data->data.string);
        snprintf(basedata_string->data.string, strlen(basedata_string->data.string), "change!");
        printf("after change base value, deep copy value:%s\n", clone_data->data.string);
出力
deep copy value:deep_copy_base, change base value
after change base value, deep copy value:deep_copy_base

使用したデータはprototype_freeで削除してくださいね。prototype_unregisterは呼ばなくてもprototype_manager_freeだけでOK

main.c
        prototype_free(deep_string_factory, clone_data);
...
        prototype_manager_free(manager);

API変更履歴

2018/06/30 初版

参考

シャローコピーとディープコピーの違い
prototypeパターン