自作クラスと Siv3D の連係¶
1. 概要¶
本記事では、自作クラスを Siv3D の様々な機能と連係させる方法を説明します。連係によって以下のようなメリットが得られます。
- Printや- Consoleなどで直接扱えるようになる
- U"{}"_fmt()で直接扱えるようになる
- INI や JSON, CSV などの設定ファイルで読み書きできるようになる
- バイナリデータを読み書きできるようになる
- HashSetや- HashTableのキーとして使えるようになる
2. フォーマット対応¶
クラスを「フォーマット可能」にすると、
- Printによる画面へのデバッグ出力
- Consoleによるコンソール出力
- Sayによる音声読み上げ
- Format()による- Stringへの変換
- TextWriterへの書き出し
- INI,- JSON,- CSVなどの設定ファイルへの書き出し
- Font::operator()
などができるようになります。
2.1 方法¶
クラスの定義内に、次のような関数を追加します。
この関数内で value を文字列化し、FormatData の String 型のメンバ変数 .string に追加します。
friend void Formatter(FormatData& formatData, const RGB& value)
{
	formatData.string += U"({}, {}, {})"_fmt(value.r, value.g, value.b);
}
これで自作クラスが「フォーマット可能」になりました。
2.2 サンプル¶
# include <Siv3D.hpp>
struct RGB
{
	float r, g, b;
	friend void Formatter(FormatData& formatData, const RGB& value)
	{
		formatData.string += U"({}, {}, {})"_fmt(value.r, value.g, value.b);
	}
};
void Main()
{
	const RGB rgb{ 0.1f, 0.2f, 0.3f };
	// 画面へのデバッグ出力
	Print << rgb;
	// コンソール出力
	Console << rgb;
	// 音声読み上げ
	Say << rgb;
	// String への変換
	const String s = Format(rgb);
	// TextWriter への書き込み
	{
		TextWriter writer{ U"test.txt" };
		writer << rgb;
	}
	// INI, JSON, CSV 等各種設定ファイルへの書き込み
	{
		INI ini;
		ini[U"aaa.color"] = rgb;
		ini.save(U"test.ini");
		JSON json;
		json[U"aaa"][U"color"] = rgb;
		json.save(U"test.json");
		CSV csv;
		csv.writeRow(U"item", U"color");
		csv.writeRow(U"aaa", rgb);
		csv.save(U"test.csv");
	}
	const Font font{ 64 };
	while (System::Update())
	{
		// Font::operator() での使用
		font(rgb).draw(100, 100);
	}
}
3. _fmt 対応¶
クラスを _fmt に対応させると、そのクラスを U"{}"_fmt() で文字列化できるようになります。
3.1 方法¶
グローバル名前空間に次のような fmt::formatter の特殊化を定義します。例として、先ほどの RGB クラスを対応させます。
template <>
struct SIV3D_HIDDEN fmt::formatter<RGB, s3d::char32>
{
	std::u32string tag;
	auto parse(basic_format_parse_context<s3d::char32>& ctx)
	{
		return s3d::detail::GetFormatTag(tag, ctx);
	}
	template <class FormatContext>
	auto format(const RGB& value, FormatContext& ctx)
	{
		return format_to(ctx.out(), U"({}, {}, {})", value.r, value.g, value.b);
	}
};
{:.2f} のような特殊なタグにも対応させる場合は次のように実装します。
template <>
struct SIV3D_HIDDEN fmt::formatter<RGB, s3d::char32>
{
	std::u32string tag;
	auto parse(basic_format_parse_context<s3d::char32>& ctx)
	{
		return s3d::detail::GetFormatTag(tag, ctx);
	}
	template <class FormatContext>
	auto format(const RGB& value, FormatContext& ctx)
	{
		if (tag.empty()) // 特殊タグが無い場合
		{
			return format_to(ctx.out(), U"({}, {}, {})", value.r, value.g, value.b);
		}
		else // 特殊タグがある場合
		{
			const std::u32string format
				= (U"({:" + tag + U"}, {:" + tag + U"}, {:" + tag + U"})");
			return format_to(ctx.out(), format, value.r, value.g, value.b);
		}
	}
};
3.2 サンプル¶
# include <Siv3D.hpp>
struct RGB
{
	float r, g, b;
};
template <>
struct SIV3D_HIDDEN fmt::formatter<RGB, s3d::char32>
{
	std::u32string tag;
	auto parse(basic_format_parse_context<s3d::char32>& ctx)
	{
		return s3d::detail::GetFormatTag(tag, ctx);
	}
	template <class FormatContext>
	auto format(const RGB& value, FormatContext& ctx)
	{
		if (tag.empty()) // 特殊タグが無い場合
		{
			return format_to(ctx.out(), U"({}, {}, {})", value.r, value.g, value.b);
		}
		else // 特殊タグがある場合
		{
			const std::u32string format
				= (U"({:" + tag + U"}, {:" + tag + U"}, {:" + tag + U"})");
			return format_to(ctx.out(), format, value.r, value.g, value.b);
		}
	}
};
void Main()
{
	const RGB rgb{ 0.111f, 0.222f, 0.333f };
	Print << U"color: {}"_fmt(rgb);
	Print << U"color: {:.1f}"_fmt(rgb);
	while (System::Update())
	{
	}
}
4. パース対応¶
クラスを「パース可能」にすると、
- Parse(),- ParseOr(),- ParseOpt()
- INI,- JSON,- CSVなどの設定ファイルからの読み込み
などができるようになります。
4.1 方法¶
クラスの定義内に、次のような関数を追加します。
template <class CharType>
friend std::basic_istream<CharType>& operator >>(std::basic_istream<CharType>& input, 自作クラス& value)
{
}
この関数内で、入力ストリーム input から値を読み込みます。フォーマットした文字列をパースできるような対称的な操作になることが望ましいです。
template <class CharType>
friend std::basic_istream<CharType>& operator >>(std::basic_istream<CharType>& input, RGB& value)
{
	CharType unused;
	return input >> unused
		>> value.r >> unused
		>> value.g >> unused
		>> value.b >> unused;
}
これで自作クラスが「パース可能」になりました。
4.2 サンプル¶
# include <Siv3D.hpp>
struct RGB
{
	float r, g, b;
	friend void Formatter(FormatData& formatData, const RGB& value)
	{
		formatData.string += U"({}, {}, {})"_fmt(value.r, value.g, value.b);
	}
	template <class CharType>
	friend std::basic_istream<CharType>& operator >>(std::basic_istream<CharType>& input, RGB& value)
	{
		CharType unused;
		return input >> unused
			>> value.r >> unused
			>> value.g >> unused
			>> value.b >> unused;
	}
};
void Main()
{
	const RGB rgb{ 0.1f, 0.2f, 0.3f };
	const String s = Format(rgb);
	const RGB rgb2 = Parse<RGB>(s);
	Print << rgb2;
	// 2.3 で作成した設定ファイルから読み込む
	{
		INI ini{ U"test.ini" };
		const RGB x = Parse<RGB>(ini[U"aaa.color"]);
		Print << U"INI: " << x;
	}
	// 2.3 で作成した設定ファイルから読み込む
	{
		JSON json = JSON::Load(U"test.json");
		const RGB x = json[U"aaa"][U"color"].get<RGB>();
		Print << U"JSON: " << x;
	}
	// 2.3 で作成した設定ファイルから読み込む
	{
		CSV csv{ U"test.csv" };
		const RGB x = csv.get<RGB>(1, 1);
		Print << U"CSV: " << x;
	}
	while (System::Update())
	{
	}
}
5. シリアライズ対応¶
クラスを「シリアライズ可能」にすると、
- Serializer
- Deserializer
で使えるようになります。
5.1 方法¶
クラスの定義内に、次のようなメンバ関数を追加します。
この関数内で、各メンバ変数を archive() に引数として渡します。各メンバ変数はシリアライズ対応している必要があります。
5.2 サンプル¶
# include <Siv3D.hpp>
struct RGB
{
	float r, g, b;
	friend void Formatter(FormatData& formatData, const RGB& value)
	{
		formatData.string += U"({}, {}, {})"_fmt(value.r, value.g, value.b);
	}
	template <class Archive>
	void SIV3D_SERIALIZE(Archive& archive)
	{
		archive(r, g, b);
	}
};
void Main()
{
	// バイナリデータをファイルに保存
	{
		const RGB rgb{ 0.1f, 0.2f, 0.3f };
		const Array<RGB> colors = { RGB{ 0.2f, 0.3f, 0.4f }, RGB{ 0.5f, 0.6f, 0.7f } };
		Serializer<BinaryWriter> writer{ U"test.bin" };
		writer(rgb);
		writer(colors);
	}
	// ファイルに保存したバイナリデータを読み込む
	{
		RGB rgb;
		Array<RGB> colors;
		Deserializer<BinaryReader> reader{ U"test.bin" };
		reader(rgb);
		reader(colors);
		Print << rgb;
		Print << colors;
	}
	while (System::Update())
	{
	}
}
6. ハッシュ対応¶
クラスをハッシュ対応させると、HashSet や HashTable のキーとして使えるようになります。
6.1 方法¶
クラスに == 演算子と、std::hash<> の特殊化を定義します。
friend bool operator ==(const 自作クラス& a, const 自作クラス& b) noexcept
{
	// a と b が等しい場合 true を返す
}
// または
bool operator ==(const 自作クラス&) const = default;
template<>
struct std::hash<自作クラス>
{
	size_t operator()(const 自作クラス& value) const noexcept
	{
		// ハッシュ値を返す
	}
};
クラスが Trivially Copyable であれば、ハッシュ値の生成には Hash::FNV1a() 関数を使うことができます。
6.2 サンプル¶
# include <Siv3D.hpp>
struct Calendar
{
	int16 month;
	int16 day;
	bool operator ==(const Calendar&) const = default;
};
template<>
struct std::hash<Calendar>
{
	size_t operator()(const Calendar& value) const noexcept
	{
		return Hash::FNV1a(value);
	}
};
void Main()
{
	HashTable<Calendar, String> table;
	table[Calendar{ 1, 1 }] = U"元旦";
	table[Calendar{ 5, 5 }] = U"こどもの日";
	table[Calendar{ 11, 3 }] = U"文化の日";
	const Calendar calendar{ 5, 5 };
	if (auto it = table.find(calendar); it != table.end())
	{
		Print << it->second;
	}
	while (System::Update())
	{
	}
}