RapidJSONについて
公式ページ:http://rapidjson.org/index.html
cocos2d-xには標準搭載されているJSONパーサーライブラリとなっています。
数年前にcocos2d-xで作成したゲーム内のデータ構造でJSONを扱うために利用しました。
RapidJSONは高いパフォーマンスで動作しますが、その仕組みを理解して使わなければ、
オブジェクト指向上扱いにくいライブラリのように感じる気がします...。
数年前に私もひどい扱い方をして、RapidJSONの特徴を台無しにしてしまった挙句、
メモリ管理ガバガバアプリケーションができてしまいました。
反省の念を込めて、RapidJSONの特徴についてまとめます。
(使い方は他の人がまとめてたりすると思うので)
特徴と問題
RapidJSONは軽量でメモリに優しく、無駄な処理を極力なくすような作りになっています。
その反面、利用方法は最近の言語と比べて難しく、きちんと理解しなくては扱いづらいです。
(C++らしいと言えばそうなのですが、、、)
今回は取り合えげるのは、直面した問題である、オブジェクトのコピーについてです。
RapidJSONの方針としては、処理効率とメモリ使用率への配慮を優先し、
原則的に単純なオブジェクトのコピーを禁止しており、メモリアロケータを利用した移動を行うという制限があります。
オブジェクトのコピーについて
公式ドキュメントより
http://rapidjson.org/md_doc_tutorial.html#MoveSemantics
A very special decision during design of RapidJSON is that, assignment of value does not copy the source value to destination value. Instead, the value from source is moved to the destination.
RapidJSONの設計中の特別な決定事項は
与えられた値をコピーすることをせず、元の値が移動されるようにした事だ!
Why? What is the advantage of this semantics?
なぜ?それはどんな利点を意味しているかって?
The simple answer is performance. For fixed size JSON types (Number, True, False, Null), copying them is fast and easy. However, For variable size JSON types (String, Array, > Object), copying them will incur a lot of overheads. And these overheads are often unnoticed. Especially when we need to create temporary object, copy it to another variable, and then destruct it.
簡単な答えとしては、パフォーマンスさ!
固定サイズのJSON型だったらコピーは素早く簡単だ。
しかし可変サイズのJSON型を扱うから、それらのコピーに対するオーバヘッドは大きくなる。
そして、このオーバーヘッドは気づきづらい。
特に一時的なオブジェクトを作成し、変数にコピーして破棄するまでの流れでな。
処理の詳細な流れは、本家のドキュメントに図とともにわかりやすく載せられていますので、そちらをご参照ください。
簡単にいうと、
値のコピーの際には、メモリのallocate/deallocateが行われてしまい、
これを参照カウンタとGC(GabageCollection)を駆使して解消するのは難しいので、
参照メモリの移動と、移動元にnullptrを入れるというシンプルな方法で解決したよ!
ということだそうです。
実際にソースコードを見てみる
Ver1.1.0
のタグのバージョンのrapidjson/document.h
について見てみると
GenericDocumentクラス(typedefでDocumentクラスとなっている)
private:
//! Prohibit copying
GenericDocument(const GenericDocument&);
//! Prohibit assignment
GenericDocument& operator=(const GenericDocument&);
このようにDocumentのオブジェクトの
- コピーコンストラクタ
- 代入演算子
がprivate
になっています。
つまり、Documentのオブジェクトのコピーができないようになっています。
(ポインタ渡しは可能なはず)
GenericValueクラス(typedefでValueクラスとなっている)
DocumentのスーパークラスであるValueについては以下のように定義されています。
まさにコピーを禁止して、移動を行う代入演算子が定義されています。(他の型についても存在しますが抜粋)
private:
//! Copy constructor is not permitted.
GenericValue(const GenericValue& rhs);
public:
//!@name Assignment operators
//@{
//! Assignment with move semantics.
/*! \param rhs Source of the assignment. It will become a null value after assignment.
*/
GenericValue& operator=(GenericValue& rhs) RAPIDJSON_NOEXCEPT {
RAPIDJSON_ASSERT(this != &rhs);
this->~GenericValue();
RawAssign(rhs);
return *this;
}
# if RAPIDJSON_HAS_CXX11_RVALUE_REFS
//! Move assignment in C++11
GenericValue& operator=(GenericValue&& rhs) RAPIDJSON_NOEXCEPT {
return *this = rhs.Move();
}
# endif
これを浅く理解した昔の自分がやった間違い
公式をforkして、以下の形で修正して使いました。
- private:
+ // private: /* 上にpublic:があるのでpublic化される */
//! Prohibit copying
GenericDocument(const GenericDocument&);
//! Prohibit assignment
GenericDocument& operator=(const GenericDocument&);
+ private: /* これより下をprivateにする */
こうすることで、Documentオブジェクトの値のコピーは許可され、プログラミング上はとても扱いやすくなりました。
しかし、C++のメモリ管理や、RapidJSONの設計思想に背いており、使い方によっては性能が劣化するやばいことになっています。
(ライブラリを使う際はドキュメントをしっかり読むのは基本中の基本ですよね、反省してます...)
公式のオブジェクトのコピーの方法
公式ドキュメントにもDOM構造のディープコピーを行う場合は以下のようにしてくれと記載があります。
SetArray()
とかPushBack()
とか実行したタイミングで、例の移動がされるようです。
Document d;
Document::AllocatorType& a = d.GetAllocator();
Value v1("foo");
// Value v2(v1); // not allowed
Value v2(v1, a); // make a copy
assert(v1.IsString()); // v1 untouched
d.SetArray().PushBack(v1, a).PushBack(v2, a);
assert(v1.IsNull() && v2.IsNull()); // both moved to d
v2.CopyFrom(d, a); // copy whole document to v2
assert(d.IsArray() && d.Size() == 2); // d untouched
v1.SetObject().AddMember("array", v2, a);
d.PushBack(v1, a);
結論
RapidJSONのDocumentオブジェクトは、純粋なコピーが発生するような扱い方ができません。
つまり、戻り値の返却などの利用で不用意にコピーが発生しないように制限されているように思えます。
(ポインタを使えば、受け渡し自体は可能)
RapidJSONの各種インターフェースは扱う際に毎回複雑な操作が必要でわかりづらいため、
他言語のライブラリなどに見受けられるようなJSON型としての利用には向いてなさそうです。
きれいなオブジェクト指向で設計されたクラスなどの利用元から見えないように、うまくラッピングして利用する必要がありそうです。
したがって、以下のようなクラスを作成することが望ましいのでないかと思います。
- 1つのJSONテキスト、すなわち1つのDocumentオブジェクトをプロパティに持つ
- Documentオブジェクトプロパティの各要素に対して、操作手段のメソッドを提供する
- クラスの外側にはRapidJSONを意識させない
- DIコンテナ管理やSingletonなどでただ一つのオブジェクトとして保証する
上記のクラスは、RDBのにおけるO/Rマッパーに近い位置付けになるのではないでしょうか。
こうすることで、きれいなオブジェクト指向の設計とRapidJSONの良さを生かしたアプリケーションが生まれることでしょう。