OpenSiv3D icon indicating copy to clipboard operation
OpenSiv3D copied to clipboard

3'000_big, 0xabp+4_bigF などをコンパイルエラーにする、もしくは対応する

Open Raclamusi opened this issue 3 years ago • 1 comments

提案

operator""_bigoperator""_bigF はそれぞれ boost::multiprecision::cpp_intboost::multiprecision::cpp_dec_float_100assign メンバ関数にパラメータを渡すので、それらが対応していない形式の文字列が渡された場合は std::runtime_error が投げられます。 これにより、 3'000_big0xabp+4_bigF などは、 C++20 のリテラルとしては正しいためコンパイルは通りますが、実行時エラーが発生します。

コンストラクタや assign メンバ関数に渡された文字列が不正な場合は仕方ありませんが、リテラルの場合、値がコンパイル時に分かるのでコンパイルエラーにできるはずです。 よって、 operator""_big および operator""_bigF にコンパイル時チェックを導入するか、 C++20 の整数リテラルおよび浮動小数点数リテラルの形式に対応することを提案します。

# include <Siv3D.hpp>

# define check(expr)  try { Print << #expr U": {}"_fmt(expr); } catch (const std::runtime_error&) { Print << #expr U": runtime error!"; }

void Main()
{
	check( 123_big );  // ok
	check( 0173_big );  // ok
	check( 0x7b_big );  // ok
	check( 0b01111011_big );  // runtime error!
	check( 1'2'3_big );  // runtime error!
	//check( 123.0_big );  // compile error (#826)

	check( 123_bigF );  // ok
	check( 0123_bigF );  // ok, ただし BigInt とは異なり10進法でパースされる
	check( 123.0_bigF );  // ok
	check( 123._bigF );  // ok
	check( .123e+3_bigF );  // ok
	check( 0x7b_bigF );  // runtime error!
	check( 0x7bp0_bigF );  // runtime error!
	check( 0b01111011_bigF );  // runtime error!
	check( 1'2'3.0_bigF );  // runtime error!

	while (System::Update());
}

コンパイルエラーにする

リテラル演算子をリテラル演算子テンプレートに変更して、文字列をコンパイル時にチェックしてエラーにする案です。

static_assert を使う

対応している形式でないとき、 static_assert でコンパイルエラーにする。

良い点

  • シンプルに実装できる
  • エラーメッセージが書ける

悪い点

  • エラーの行番号が static_assert を書いた行のものになる

image

実装例

クリックでコードを開く/閉じる
// BigInt.hpp
	inline namespace Literals
	{
		inline namespace BigNumLiterals
		{
			template <char... S>
			[[nodiscard]]
			BigInt operator ""_big();

			[[nodiscard]]
			BigInt operator ""_big(long double) = delete;
		}
	}
// BigFloat.hpp
	inline namespace Literals
	{
		inline namespace BigNumLiterals
		{
			template <char... S>
			[[nodiscard]]
			BigFloat operator ""_bigF();
		}
	}
// detail/BigInt.ipp
	namespace detail
	{
		// テンプレート特殊化でチェックする例
		// 美しいが、複雑なものには向かない

		template <char... S>
		struct IsDecimalLiteral : std::bool_constant<(IsDigit(S) && ...)> {};
		template <>
		struct IsDecimalLiteral<> : std::bool_constant<false> {};

		template <char... S>
		struct IsHexadecimalLiteral : std::bool_constant<false> {};
		template <char X>
		struct IsHexadecimalLiteral<'0', X> : std::bool_constant<false> {};
		template <char... S>
		struct IsHexadecimalLiteral<'0', 'x', S...> : std::bool_constant<(IsXdigit(S) && ...)> {};
		template <char... S>
		struct IsHexadecimalLiteral<'0', 'X', S...> : std::bool_constant<(IsXdigit(S) && ...)> {};

		template <char... S>
		constexpr bool IsBigIntLiteral = IsDecimalLiteral<S...>::value || IsHexadecimalLiteral<S...>::value;
	}

	inline namespace Literals
	{
		inline namespace BigNumLiterals
		{
			template <char... S>
			BigInt operator ""_big()
			{
				static_assert(detail::IsBigIntLiteral<S...>);

				using CSTR = const char[];
				return BigInt{ std::string_view{ CSTR{ S..., '\0' } } };
			}
		}
	}
// detail/BigFloat.ipp
	namespace detail
	{
		// constexpr 関数でチェックする例
		// 美しくないが、複雑なものにも対応できる

		template <char... S>
		constexpr bool IsBigFloatLiteral()
		{
			using CSTR = const char[];
			const std::string_view s{ CSTR{ S..., '\0' } };
			auto it = s.begin();
			const auto end = s.end();

			// (\.\d+|\d+\.?\d*)([eE][+-]?\d+)?

			// (\.\d+|\d+\.?\d*)
			if (it == end) return false;
			if (*it == '.')  // \.\d+
			{
				// \.
				++it;
				// \d+
				if (it == end || not IsDigit(*it)) return false;
				++it;
				while (it != end && IsDigit(*it)) ++it;
			}
			else  // \d+\.?\d*
			{
				// \d+
				if (it == end || not IsDigit(*it)) return false;
				++it;
				while (it != end && IsDigit(*it)) ++it;
				// \.?
				if (it != end && *it == '.') ++it;
				// \d*
				while (it != end && IsDigit(*it)) ++it;
			}

			// ([eE][+-]?\d+)?
			if (it != end)  // [eE][+-]?\d+
			{
				// [eE]
				if (it == end || ToLower(*it) != 'e') return false;
				++it;
				// [+-]?
				if (it != end && (*it == '+' || *it == '-')) ++it;
				// \d+
				if (it == end || not IsDigit(*it)) return false;
				++it;
				while (it != end && IsDigit(*it)) ++it;
			}

			return it == end;
		}
	}

	inline namespace Literals
	{
		inline namespace BigNumLiterals
		{
			template <char... S>
			BigFloat operator ""_bigF()
			{
				static_assert(detail::IsBigFloatLiteral<S...>());

				using CSTR = const char[];
				return BigFloat{ std::string_view{ CSTR{ S..., '\0' } } };
			}
		}
	}

chars3d::char32 に暗黙キャストしてるの、不安すぎる)

std::enable_if を使う

static_assert の代わりに std::enable_if を使う。

良い点

  • エラーにリテラルを書いた行の行番号が出る

悪い点

  • リテラル演算子テンプレートの宣言からすべてのチェックの実装が見えている必要があるので、 detail に逃がせず、ヘッダが散らかる

image

実装例

クリックでコードを開く/閉じる
// BigInt.hpp
	// static_assert のほうの実装例で定義したものを含める
	inline namespace Literals
	{
		inline namespace BigNumLiterals
		{
			template <char... S>
			[[nodiscard]]
			std::enable_if_t<detail::IsBigIntLiteral<S...>, BigInt>
			operator ""_big()
			{
				using CSTR = const char[];
				return BigInt{ std::string_view{ CSTR{ S..., '\0' } } };
			}

			[[nodiscard]]
			BigInt operator ""_big(long double) = delete;
		}
	}
// BigFloat.hpp
	// static_assert のほうの実装例で定義したものを含める
	inline namespace Literals
	{
		inline namespace BigNumLiterals
		{
			template <char... S>
			[[nodiscard]]
			std::enable_if_t<detail::IsBigFloatLiteral<S...>(), BigFloat>
			operator ""_bigF()
			{
				using CSTR = const char[];
				return BigFloat{ std::string_view{ CSTR{ S..., '\0' } } };
			}
		}
	}

対応する

Boost が対応してないなら、 Siv3D が対応するのもありかなと思います。

BigInt::assign, BigFloat::assign を変更する

対応する必要があるものを挙げます。

  • ' を含む整数、浮動小数点数
    • ' を無視するだけでいいので、楽そう
  • 2進数リテラル
    • 16進数は boost::multiprecision::cpp_int が対応してるので、16進数に置き換えると楽そう
  • 16進浮動小数点数リテラル
    • 仮数部と指数部を分けて、 BigInt{ (仮数部) } * Math::Pow(BigFloat{ 2 }, BigFloat{ (指数部) }) をすればよさそう
  • (8進数リテラル)
    • エラーにはならないが、 BigInt だと8進数、 BigFloat だと10進数扱いなので、対応した方がいいかも
    • でも C++ も 0123 == 83 だけど 0123. == 123 みたいな感じなので、対応しなくてもいいかも

operator""_big, operator""_bigF を変更する

文字列→ BigInt / BigFloat の変換のパフォーマンスを悪くしないために、コンパイル時に処理できるリテラルでだけ対応するのもいいと思います。

Raclamusi avatar Jun 19 '22 16:06 Raclamusi

おお、これはこだわる人が少ないと想定されるため優先度は高くないですが、興味深い課題です。 優先度の高い課題がある程度片付いたら本格的な検討を進めます。 何かアップデートがあればこの Issue にコメントをつなげていってください。

Reputeless avatar Jun 19 '22 16:06 Reputeless