[C++] bool 型を強くする YesNo クラス

2021/12/22に公開

これは C++ Advent Calendar 2021 の参加記事です。

C++ のプログラムを少しだけ読みやすく、安全にするために作った便利クラス YesNo<T> と、それを簡単に使えるライブラリ siv::YesNo (Boost Software License 1.0) を公開します。このライブラリは Siv3D の API で長らく使われている類似のクラス s3d::YesNo<T> を、再利用しやすく切り出したものです。

https://github.com/Reputeless/YesNo

1. よくある問題

bool 型の引数は、呼び出し側からは意味が分からなかったり、取り違えたりするおそれがあります。

void ToHex(int n, bool uppercase)
{
	if (uppercase)
		std::cout << "uppercase\n";
	else
		std::cout << "lowercase\n";
	//...
}

void DoTask(bool formatDisk, bool writeLog)
{
	if (formatDisk)
		std::cout << "Formatting a disk...\n";
	if (writeLog)
		std::cout << "Writing a log...\n";
}

int main()
{
	ToHex(255, true); // この true は何?

	bool writeLog = true;

	DoTask(writeLog, false); // もしかして取り違えてない?
}

2. enum class を使ったコード

enum class: bool で名前や型を与えてあげると問題を解消できますが、if (e) のように書けない不便が発生します。条件演算子 (?:) でも使えません。

enum class Uppercase: bool { False, True }; // これを
enum class FormatDisk: bool { False, True }; // 毎回記述するのは
enum class WriteLog: bool { False, True };  // 面倒

void ToHex(int n, Uppercase uppercase)
{
	if (uppercase) // コンパイルエラー: if (e) はできない
		std::cout << "uppercase\n";
	else
		std::cout << "lowercase\n";
	//...
}

void DoTask(FormatDisk formatDisk, WriteLog writeLog)
{
	if (formatDisk == FormatDisk::True) // == を使うか,
		std::cout << "Formatting a disk...\n";
	if (static_cast<bool>(writeLog)) // bool 型にキャストする
		std::cout << "Writing a log...\n";
}

int main()
{
    ToHex(255, Uppercase::True);

    DoTask(FormatDisk::False, WriteLog::True);
}

3. YesNo<T> を使ったコード

YesNo<T> を使うと、enum class のように型や名前を与えつつ、bool 型の値であるかのように if () や条件演算子の中で使うことができます。

using Uppercase = YesNo<struct Uppercase_tag>;
using FormatDisk = YesNo<struct FormatDisk_tag>;
using WriteLog = YesNo<struct WriteLog_tag>;

void ToHex(int n, Uppercase uppercase)
{
	if (uppercase) // ok
		std::cout << "uppercase\n";
	else
		std::cout << "lowercase\n";
	//...
}

void DoTask(FortmatDisk formatDisk, WriteLog writeLog)
{
	if (formatDisk) // ok
		std::cout << "Formatting a disk...\n";
	if (writeLog) // ok
		std::cout << "Writing a log...\n";
}

int main()
{
    ToHex(255, Uppercase::Yes);

    DoTask(FormatDisk::No, WriteLog::Yes);
}

4. 実装

実装はシンプルです。
https://github.com/Reputeless/YesNo/blob/main/YesNo.hpp

実質的に struct { bool; } なので、最適化が有効であればオーバーヘッドは発生しません。

5. 使い方

  1. "YesNo.hpp" をインクルード
  2. (必要に応じて) using siv::YesNo;
  3. using XXXX = YesNo<struct XXXX_tag>;
  4. boolXXXX に、true, falseXXXX::Yes, XXXX::No に置き換える
# include <iostream>
# include "YesNo.hpp"

using siv::YesNo;
using Uppercase = YesNo<struct Uppercase_tag>;
using FormatDisk = YesNo<struct FormatDisk_tag>;
using WriteLog = YesNo<struct WriteLog_tag>;

void ToHex(int n, Uppercase uppercase)
{
	if (uppercase)
		std::cout << "uppercase\n";
	else
		std::cout << "lowercase\n";
	//...
}

void DoTask(FormatDisk formaDisk, WriteLog writeLog)
{
	if (formaDisk)
		std::cout << "Formatting a disk...\n";
	if (writeLog)
		std::cout << "Writing a log...\n";
}

int main()
{
	ToHex(255, Uppercase::Yes);
	ToHex(255, Uppercase::No);

	Uppercase uppercase = Uppercase::No;
	ToHex(255, uppercase);

	FormatDisk formatDisk = FormatDisk::Yes;
	ToHex(255, formatDisk); // コンパイルエラー

	DoTask(FormatDisk::No, WriteLog::Yes);
	DoTask(FormatDisk{ false }, WriteLog{ true });

	DoTask(WriteLog::Yes, FormatDisk::No); // コンパイルエラー
	DoTask(false, true); // コンパイルエラー
}

6. おもな API

true / false を表現する定数 Yes / No.

int main()
{
	constexpr FormatDisk f1 = FormatDisk::Yes;
	constexpr FormatDisk f2 = FormatDisk::No;
}

bool 型の値からの初期化

int main()
{
	constexpr bool b1 = true, b2 = false;
	constexpr FormatDisk f1{ b1 };
	constexpr FormatDisk f2{ b2 };
}

explicit operator bool()

int main()
{
	constexpr FormatDisk formatDisk = FormatDisk::No;
	constexpr WriteLog writeLog = WriteLog::Yes;

	if (formatDisk) {}

	if (formatDisk || writeLog) {}

	if (!formatDisk) {}

	constexpr int n = writeLog ? 100 : 200;

	static_assert(writeLog);
}

▼ 予期しない変換を防ぐため、contextually converted to bool でない文脈では明示的に .getBool() を使う必要があります。

void EnableLog(bool enable) {}

int main()
{
	constexpr FormatDisk formatDisk = FormatDisk::No;
	constexpr WriteLog writeLog = WriteLog::Yes;

	constexpr WriteLog w1{ formatDisk }; // コンパイルエラー
	constexpr WriteLog w2{ formatDisk.getBool() };

	constexpr bool b1 = formatDisk; // コンパイルエラー
	constexpr bool b2 = formatDisk.getBool();

	EnableLog(writeLog); // コンパイルエラー
	EnableLog(writeLog.getBool());
}

▼ 同じ型どうしで比較ができます。

int main()
{
	constexpr FormatDisk formatDisk = FormatDisk::No;
	constexpr WriteLog writeLog1 = WriteLog::Yes;
	constexpr WriteLog writeLog2 = WriteLog::No;

	if (writeLog1 == writeLog2) {}

	if (writeLog1 > writeLog2) {}

	if (formatDisk == writeLog1) {} // コンパイルエラー
}

7. Siv3D の API での使用例

# include <Siv3D.hpp> // OpenSiv3D v0.6.3

void Main()
{
	// ストップウォッチを即座に開始
	Stopwatch stopwatch{ 15s, StartImmediately::Yes };

	Image image{ 256, 256, Palette::White };

	// アンチエイリアスを有効にして円を画像に描き込む
	Circle{ 128, 128, 60 }.paint(image, Palette::Orange, Antialiased::Yes);

	// 画像を WebP のロスレス形式で保存
	image.saveWebP(U"image.webp", Lossless::Yes);

	// ファイルをゴミ箱に送らず削除する
	FileSystem::Remove(U"image.webp", AllowUndo::No);

	const LineString lines
	{
		Vec2{ 500, 100 }, Vec2{ 700, 200 }, Vec2{ 600, 500 },
	};

	// 各点を結んだ連続する線分の長さを計算(終点と始点をつないでリング状にする)
	const double length = lines.calculateLength(CloseRing::Yes);
}

8. ライブラリ (Boost Software License 1.0)

今回紹介した YesNo<T> クラスを簡単に利用できる siv::YesNo ライブラリを下記のリポジトリで公開しました。Boost Software License 1.0 です。不具合や改善案の報告は Issues までお願いします。

https://github.com/Reputeless/YesNo

Discussion