コンテンツにスキップ

12. Siv3D の C++ API 設計

このページでは、Siv3D の C++ API の設計のなかで興味深い部分を紹介します。

12.1 ユーザ定義リテラル

ユーザ定義リテラルは C++11 から入った機能で、リテラル値(123, 4.56, "abc" など)のサフィックス (末尾につく英数字) を新しく創作できます。仕組みとしては、123_●●(任意のサフィックス) が、別途用意した 何らかの関数(123) の糖衣構文になります。

活用事例 ① 度数法リテラル

例えば、プログラムでは通常、角度をラジアンで扱いますが、人間が角度を理解するのは度数法です。度数法の数値リテラルからラジアンに変換する関数を用意し、それをリテラルとして使えるようにしています。

before
# include <Siv3D.hpp>

void Main()
{
	double angle = Math::ToRadians(90);

	Print << angle;

	while (System::Update())
	{

	}
}
after
# include <Siv3D.hpp>

void Main()
{
	double angle = 90_deg;

	Print << angle;

	while (System::Update())
	{

	}
}

活用事例 ② 絵文字リテラル

通常の文字列リテラルの場合に「ファイルパス」と解釈し、そのパスから画像をロードします。一方、_emoji サフィックスをつけたものは絵文字を表す文字列リテラルとして解釈し、内蔵の絵文字フォントから該当する絵文字のグリフをロードします。

# include <Siv3D.hpp>

void Main()
{
	const Texture texture1{ U"example/windmill.png" };
	const Texture texture2{ U"🐈"_emoji };
	
	while (System::Update())
	{
		texture1.draw(0, 0);
		texture2.draw(500, 200);
	}
}

活用事例 ③ フォーマットリテラル

"..."_fmt は、関数呼び出し演算子 operator() を持つ値を返すことで、"..."_fmt(...) のように使うことができます。_fmt(...) 内の値を文字列化して、"..." の中にある {} に埋め込むことができます。

# include <Siv3D.hpp>

void Main()
{
	const String name = U"Tom";
	const int32 score = 100;

	Print << U"{}'s score is {}."_fmt(name, score);

	while (System::Update())
	{

	}
}

活用事例 ④ 時間リテラル(C++ 標準ライブラリ)

C++ 標準ライブラリで提供される時間リテラル 123s, 456ms から生成される時間型 std::chrono::seconds, std::chrono::milliseconds なども、Siv3D の API と密接に連係します。

# include <Siv3D.hpp>

void Main()
{
	// 時間の計算
	const auto time = 1s + 500ms;

	// 時間の表示
	Print << time;

	while (System::Update())
	{

	}
}
# include <Siv3D.hpp>

void Main()
{
	// 10 秒のタイマーを開始する
	Timer timer{ 10s, StartImmediately::Yes };

	while (System::Update())
	{
		ClearPrint();

		// 残り時間の表示
		Print << timer;

		// タイマーが 0 になったら
		if (timer.reachedZero())
		{
			Print << U"Time's up!";
		}
	}
}

12.2 演算子オーバーロードの積極的な活用

Siv3D では、数学以外の用途でもとくに効果的だと考えられる場面で、演算子オーバーロードを積極的に活用しています。

活用事例 ① ストップウォッチの経過時間比較

時間型と時間計測クラスの直接比較を可能にしています。

before
# include <Siv3D.hpp>

void Main()
{
	Stopwatch stopwatch{ StartImmediately::Yes };

	while (System::Update())
	{
		ClearPrint();

		if (1500 <= stopwatch.ms())
		{
			Print << U"1.5 秒経過";
		}
	}
}
after
# include <Siv3D.hpp>

void Main()
{
	Stopwatch stopwatch{ StartImmediately::Yes };

	while (System::Update())
	{
		ClearPrint();

		if (1.5s <= stopwatch)
		{
			Print << U"1.5 秒経過";
		}
	}
}

活用事例 ② 入力の組み合わせ

KeyAMouseL などの入力の組み合わせを、演算子 +| で表現できるようにしています。

  • + を使う例
before
# include <Siv3D.hpp>

void Main()
{
	while (System::Update())
	{
		if (KeyShift.pressed() && KeyS.down())
		{
			Print << U"Shift + S が押された";
		}
	}
}
after
# include <Siv3D.hpp>

void Main()
{
	while (System::Update())
	{
		if ((KeyShift + KeyS).down())
		{
			Print << U"Shift + S が押された";
		}
	}
}
  • | を使う例
# include <Siv3D.hpp>

void Main()
{
	// エンターキー、スペースキー、マウスの左ボタン、XBbox コントローラーの A ボタンのいずれか
	const auto okInput = (KeyEnter | KeySpace | MouseL | XInput(0).buttonA);

	while (System::Update())
	{
		if (okInput.down())
		{
			Print << U"決定";
		}
	}
}

活用事例 ③ 配列への push_back

Array クラスに対して、末尾に要素を追加する .push_back(x) を演算子 << で表現できるようにしています。

before
# include <Siv3D.hpp>

void Main()
{
	Array<Circle> circles;

	while (System::Update())
	{
		if (MouseL.down())
		{
			circles.push_back(Circle{ Cursor::Pos(), 10 });
		}

		for (const auto& circle : circles)
		{
			circle.draw();
		}
	}
}
after
# include <Siv3D.hpp>

void Main()
{
	Array<Circle> circles;

	while (System::Update())
	{
		if (MouseL.down())
		{
			circles << Circle{ Cursor::Pos(), 10 };
		}

		for (const auto& circle : circles)
		{
			circle.draw();
		}
	}
}

12.3 YesNo 型

関数の実引数で使う bool 型のリテラル true, false は、引数の取り違えや意味の取り違えを起こすことがあります。

引数を取り違える例
void F(bool clearAllData, bool enableLogging) { ... }

void Main()
{
	bool useLogging = true;

	F(useLogging, false); // 👈😺
}
引数の意味を取り違える例
void F(bool clearAllData, bool enableLogging) { ... }

void Main()
{
	bool keepData = true;

	F(keepData, true); // 👈😺
}

Siv3D では、bool 型特化の strong-typedef に相当する YesNo 型を用意しています。YesNo 型は、true に相当する 名前::Yes と、false に相当する 名前::No の 2 つの値を持ち、異なる名前では相互に代入できないような仕組みを提供します。

これにより、引数の取り違えや意味の取り違えを防ぐことができます。

void F(ClearAllData clearAllData, EnableLogging enableLogging) { ... }

void Main()
{
	F(ClearAllData::No, EnableLogging::Yes);

	// F(EnableLogging::Yes, ClearAllData::No); // コンパイルエラー
}

何が true で何が false なのかを明示的に示せることで、コードの表現力も向上します。

# include <Siv3D.hpp>

void Main()
{
	// 10 秒のタイマーを即座に開始する
	// Timer timer{ 10s, true };
	Timer timer{ 10s, StartImmediately::Yes };

	while (System::Update())
	{
		ClearPrint();

		// 残り時間の表示
		Print << timer;
	}
}

C++ における YesNo 型の作り方や使い方は、次の記事を参照してください。

12.4 名前付き引数

C++ には本来、名前付き引数の機能はありませんが、Siv3D では名前付き引数を模倣するための仕組みを提供しています。

例えば、長方形の定義においては、中心や左下、右下を基準にするほうが便利なケースもあります。しかし、C++ のオーバーロード解決は引数の型と個数の対応関係よって行われるため、左上座標を基準とするコンストラクタと、それとは別の位置を基準とするコンストラクタは共存できません。

エラーになる例
struct Rect
{
	// 左上座標を基準とするコンストラクタ
	Rect(const Point& pos, const Size& size);

	// 中心座標を基準とするコンストラクタ
	Rect(const Point& center, const Size& size); // コンパイルエラー
};

Siv3D では、コードの記述性に影響を与えずにコンストラクタや関数のオーバーロードを増やす方法として、関数の呼び出し側が引数とタグをセットにしたオブジェクトを簡易に構築できる、名前付き引数の仕組みを提供しています。

# include <Siv3D.hpp>

void Main()
{
	// 中心が (400, 300) でサイズが (300, 200) の長方形
	const Rect rect1{ Arg::center(400, 300),  300, 200 };

	// 左下が (400, 300) で幅が 200、高さが 100 の長方形
	const Rect rect2{ Arg::bottomLeft(400, 300),  200, 100 };

	while (System::Update())
	{
		rect1.draw(Palette::Green);
		rect2.draw(Palette::Blue);

		// カーソルの位置を左中心とする、幅 200、高さ 100 の長方形
		Rect{ Arg::leftCenter = Cursor::Pos(), 200, 100 }.draw(Palette::Red);
	}
}

仕組み

Arg::centerArg::bottomLeft は、メンバ関数 operator =operator () を持つ定数です。

これらへの = は、一見代入に見えても、実際は代入は行っていません。operator =operator () は、引数を受け取り、それを centerbottomLeft それぞれのタグとセットにしたオブジェクトを返します。

タグが異なれば、異なる型として扱われるため、異なるコンストラクタや関数をオーバーロードで呼び分けることができます。