はかせのラボ

私の頭の中を書いていく雑記ブログです

RapidXml XMLを読み込み自動で自作クラスを生成する

あいさつ

どうも、はかせです。

RapidXmlって便利で使いやすいんですけど
随所にレガシー臭がするところがあって
その一つに出てくるデータが全部char*ってところがありました。

随時受け手側でノード探索&変換してーってやってもいいんですが
ぶっちゃけめんどくさいんで自動化してみました。

実装

細かい話はあとにするとして、
まずはものをどうぞ
(今回作ったデータとかは割愛)

class RapidXmlTest
{
public:
	template <class T>
	static void Parse(const char* inFileName, std::vector<T>* inReturnVector);
};

template<class T>
inline void RapidXmlTest::Parse(const char * inFileName, std::vector<T>* inReturnVector)
{
	//Xmlを読み込む
	rapidxml::xml_document<> doc;
	//RapidXmlのユーティリティクラス 読み込んだXmlファイルをオンメモリに保持してくれる
	rapidxml::file<> file(inFileName);
	try
	{
		//Xmlデータをパースする
		doc.parse<0>(file.data());
	}
	catch (rapidxml::parse_error& err)
	{
		//エラーが出たら原因と場所(ポインタ)を教えてくれる
		std::cout << err.what() << " " << err.where<char*>();
		return;
	}

	//クラスのスタティック変数で宣言したノード単位で移動し中身を取り出す
	for (rapidxml::xml_node<>* child = doc.first_node()->first_node(); child; child = child->next_sibling(T::NodeName))
	{
		//ループ開始時に初期化
		T obj;
		//各クラスのパースメソッドを呼び出す
		obj.Parse(child);
		//配列に格納
		inReturnVector->push_back(obj);
	}
}

使い方

#include "pch.h"
#include "RapidXmlTest.h"

int main()
{
	std::vector<GameObject> objs;
	RapidXmlTest::Parse("TitleSceneData.xml", &objs);
}

以上で処理部のコードは以上です。
やってることは
・RapidXmlでXMLファイルをノードデータにパース
・各クラスのParseメソッドを呼び出す
・ポインタ渡しで受け取ったvectorにパースしたデータを格納する

一つ目と三つ目に関しては説明は不要だと思いますので今回は割愛します。
二つ目の各クラスのParseメソッドってのはなんぞやって話ですね。
Parseメソッドは各クラスに設定する
ノードデータをそのクラスに変換するメソッドです。

//今回作ったXmlデータを格納する構造体
struct GameObject
{
public:
	static const char* NodeName;
	std::string Name;
	std::string Tag;
	Float3 Position;
	Float3 Rotation;
	Float3 Scale;
	void Parse(rapidxml::xml_node<>* node)
	{
		//xyzの各要素を格納するポインタ
		char* x = nullptr;
		char* y = nullptr;
		char* z = nullptr;
		//名前
		Name = node->first_node("Name")->value();
		//タグ
		Tag = node->first_node("Tag")->value();
		//座標
		auto position = node->first_node("Position");
		x = position->first_node("x")->value();
		y = position->first_node("y")->value();
		z = position->first_node("z")->value();
		Position.SetFloat3FromChar(x, y, z);
		//回転
		auto rotation = node->first_node("Rotation");
		x = rotation->first_node("x")->value();
		y = rotation->first_node("y")->value();
		z = rotation->first_node("z")->value();
		Rotation.SetFloat3FromChar(x, y, z);
		//大きさ
		auto scale = node->first_node("Scale");
		x = scale->first_node("x")->value();
		y = scale->first_node("y")->value();
		z = scale->first_node("z")->value();
		Scale.SetFloat3FromChar(x, y, z);
	}
};

//cppに書く
const char* GameObject::NodeName = "GameObject";

RapidXmlのノードを受け取りそのデータを愚直にパースしてます。
Float3はただfloatを3つもってるだけのヘルパークラスです。
そしてstatic宣言しているNodeName
そのクラスを構成するデータを持つ大きなノードの名前です。
GameObjectクラスなら
GameObjectタグ配下の全ノードが対象みたいな感じです。

ちなみにstatic宣言した変数の初期化は
そのクラスの宣言スコープの外でのみできます。
ただ愚直にヘッダに書くとLNK2005エラーが出てくるので
cppファイルに書くことでエラーを回避しています。

長々と理屈を書きましたが要約すると、
・NodeNameというstatic宣言のconst char*データを変数に持つ
・Parseというxml_node<>を引数にもつメソッドを持つ

この二つの条件を満たした自作クラスなら
XMLから自動生成できるようになりました!
(これがやりたかった!)

あとがき

今回はXMLから自作クラスを自動生成できるようにした話でした。
クラスの宣言とかが煩わしい部分ではありますが、
必要になったら随時パースするよりは楽・・・なはずですw

あと今回の実装でテンプレート+ダックタイピングのテクニックや
static変数やメソッドの扱いなんかの理解も深まりました。

それでは今回はこの辺でノシ

参考

static変数をcppファイルに定義した理由
teratail.com

RapidXmlTest::Parseメソッドがvectorをポインタでもらっている理由
nonbiri-tereka.hatenablog.com