diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index f799144..6aed163 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -31,6 +31,7 @@ * xref:signed_integers.adoc[] * xref:bounded_uint.adoc[] * xref:bounded_int.adoc[] +* xref:bounded_float.adoc[] * xref:cuda.adoc[] * xref:literals.adoc[] * xref:limits.adoc[] diff --git a/doc/modules/ROOT/pages/bounded_float.adoc b/doc/modules/ROOT/pages/bounded_float.adoc new file mode 100644 index 0000000..732da43 --- /dev/null +++ b/doc/modules/ROOT/pages/bounded_float.adoc @@ -0,0 +1,152 @@ +//// +Copyright 2026 Matt Borland +Distributed under the Boost Software License, Version 1.0. +https://www.boost.org/LICENSE_1_0.txt +//// + +[#bounded_float] + += Bounded Floating-Point Types + +[IMPORTANT] +.Compiler Requirements +==== +`bounded_float` uses floating-point values as non-type template parameters, a feature added in C++20 (P1907R1). Compilers that have not yet implemented this paper -- notably *Clang 13, 14, and 15* -- cannot use `bounded_float` and the type is unavailable on those toolchains. Known-good versions are *GCC 10+*, *Clang 16+*, and *MSVC 19.30+*. + +The header `` defines the macro `BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT` to `1` when the feature is available and `0` otherwise. Code that needs to be portable across compilers should guard `bounded_float` usage with this macro: + +[source,c++] +---- +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT +boost::safe_numbers::bounded_float<-1.0f, 1.0f> x {boost::safe_numbers::f32{0.5f}}; +#endif +---- + +When `BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT` is `0`, including the header is still safe -- it simply does not expand any declarations, and the trait specializations and `numeric_limits` partial specialization for `bounded_float` are also elided. +==== + +== Description + +`bounded_float` is a compile-time bounded floating-point type that enforces value constraints at both compile time and runtime. +The bounds `Min` and `Max` are non-type template parameters of type `float` or `double`. + +The underlying storage type (`basis_type`) follows the type of the bounds: + +|=== +| Bound Type | Basis Type + +| `float` | `f32` +| `double` | `f64` +|=== + +This differs from `bounded_int` / `bounded_uint`, which select the smallest integer type that fits the range. For floating-point bounds the bound type expresses precision intent, so `bounded_float<0.0, 1.0>` (double bounds) is stored as `f64` rather than being silently downgraded to `f32`. Users wanting `f32` storage should write `bounded_float<0.0f, 1.0f>`. + +NaN bounds are rejected at the concept level via `Min == Min` (which is false for NaN). Infinity bounds are accepted, but defeat the post-arithmetic bounds check, so they are typically not useful. + +== Synopsis + +[source,c++] +---- +#include + +namespace boost::safe_numbers { + +template + requires (valid_float_bound && + valid_float_bound && + std::is_same_v && + float_raw_value(Min) == float_raw_value(Min) && // not NaN + float_raw_value(Max) == float_raw_value(Max) && // not NaN + float_raw_value(Max) > float_raw_value(Min)) +class bounded_float { +public: + using basis_type = /* f32 if decltype(Min) == float, else f64 */; + + // Construction (throws std::domain_error if NaN, signaling-NaN, or out of range) + explicit constexpr bounded_float(basis_type val); + explicit constexpr bounded_float(underlying_type val); + + // Conversions to fundamental float / double + template + explicit constexpr operator T() const; + + template + explicit constexpr operator bounded_float() const; + + // Direct accessor for the basis type (since static_cast(...) cannot + // be used: float_basis has a deleted catch-all constructor that intercepts it). + constexpr auto to_basis() const noexcept -> basis_type; + + // Comparison (defaulted) + friend constexpr auto operator==(bounded_float, bounded_float) noexcept -> bool = default; + friend constexpr auto operator<=>(bounded_float, bounded_float) noexcept -> std::partial_ordering = default; + + // Arithmetic (throw on IEEE 754 issues or out-of-range result) + friend constexpr auto operator+(bounded_float, bounded_float) -> bounded_float; + friend constexpr auto operator-(bounded_float, bounded_float) -> bounded_float; + friend constexpr auto operator*(bounded_float, bounded_float) -> bounded_float; + friend constexpr auto operator/(bounded_float, bounded_float) -> bounded_float; + friend constexpr auto operator%(bounded_float, bounded_float) -> bounded_float; + + // Compound assignment + constexpr auto operator+=(bounded_float) -> bounded_float&; + constexpr auto operator-=(bounded_float) -> bounded_float&; + constexpr auto operator*=(bounded_float) -> bounded_float&; + constexpr auto operator/=(bounded_float) -> bounded_float&; + constexpr auto operator%=(bounded_float) -> bounded_float&; +}; + +} // namespace boost::safe_numbers +---- + +`bounded_float` does not provide unary `+`, unary `-`, `++`, `--`, or any bitwise operators because the underlying `float_basis` does not provide them either; the design rule is that `bounded_float` exposes only what `floats.hpp` already supports. + +== Exception Behavior + +|=== +| Condition | Exception Type + +| Value outside `[Min, Max]` at construction or after arithmetic | `std::domain_error` +| NaN at construction | `std::domain_error` +| Signaling NaN at construction | `std::domain_error` +| Addition / subtraction overflow to +infinity | `std::overflow_error` +| Addition / subtraction underflow to -infinity | `std::underflow_error` +| Multiplication overflow | `std::overflow_error` +| Multiplication underflow | `std::underflow_error` +| Division producing NaN (e.g., 0/0, inf/inf) | `std::domain_error` +| Division by zero (finite numerator) | `std::domain_error` +| Modulo with zero divisor | `std::domain_error` +| Modulo with infinite numerator | `std::domain_error` +| Narrowing conversion (e.g., f64 -> f32) overflowing to infinity | `std::overflow_error` +|=== + +The IEEE 754 error handling for arithmetic is delegated to `float_basis`, which categorizes results as `overflow`, `underflow`, `nan_op`, `invalid_op`, or `divide_by_zero` and throws the corresponding `std::*_error`. After the arithmetic succeeds, `bounded_float` re-validates the result against `[Min, Max]` via its constructor. + +== Mixed-Width Operations + +Operations between `bounded_float` types with different bounds are compile-time errors: + +[source,c++] +---- +bounded_float<-1.0f, 1.0f> a {f32{0.5f}}; +bounded_float<-2.0f, 2.0f> b {f32{0.5f}}; + +// auto c = a + b; // Compile error: different bounds +---- + +== Notes on `static_cast` to the basis type + +`float_basis` (the implementation of `f32` / `f64`) declares an explicitly-deleted catch-all constructor that intercepts any `static_cast(other)` where `other` is not already a `float`. As a result, `static_cast(my_bounded_float)` will fail to compile with a "uses deleted function" error, even though a conversion operator exists. Use one of: + +[source,c++] +---- +auto raw {static_cast(my_bounded_float)}; // converts to fundamental float +auto basis {my_bounded_float.to_basis()}; // returns f32 directly +auto built {f32{static_cast(my_bounded_float)}}; // explicit f32 build +---- + +== Standard Library Support + +`bounded_float` participates in the same `library_type` concept as the integer bounded types, so the existing `iostream` operators, `std::formatter`, and `fmt::formatter` specializations work transparently. `std::numeric_limits>` is also specialized in ``, with `is_iec559`, `has_infinity`, `has_quiet_NaN`, and `has_signaling_NaN` set to `false` to reflect that the type rejects those values. diff --git a/include/boost/safe_numbers/bounded_floats.hpp b/include/boost/safe_numbers/bounded_floats.hpp new file mode 100644 index 0000000..6401df5 --- /dev/null +++ b/include/boost/safe_numbers/bounded_floats.hpp @@ -0,0 +1,319 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifndef BOOST_SAFE_NUMBERS_BOUNDED_FLOATS_HPP +#define BOOST_SAFE_NUMBERS_BOUNDED_FLOATS_HPP + +#include +#include +#include +#include +#include + +#ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE + +#include +#include +#include +#include +#include +#include + +#endif // BOOST_SAFE_NUMBERS_BUILD_MODULE + +// bounded_float requires C++20 floating-point non-type template parameters (P1907R1). +// On compilers without that support (notably Clang 13-15) this header expands to +// nothing, and any reference to bounded_float will fail with a "no such type" error. +// Users can test for availability with BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT. +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +namespace boost::safe_numbers { + +template + requires (detail::valid_float_bound && + detail::valid_float_bound && + std::is_same_v && + detail::float_raw_value(Min) == detail::float_raw_value(Min) && + detail::float_raw_value(Max) == detail::float_raw_value(Max) && + detail::float_raw_value(Max) > detail::float_raw_value(Min)) +class bounded_float +{ +public: + + // basis_type follows the bound's underlying type rather than fitting smallest type like for integers + // This preserves the precision that the user may want + using basis_type = std::conditional_t< + std::is_same_v, float>, + f32, f64>; + +private: + + using underlying_type = detail::underlying_type_t; + basis_type basis_ {}; + +public: + + explicit constexpr bounded_float(const basis_type val) + { + const auto raw {static_cast(val)}; + + // NaN comparisons are unordered: a naked range check would silently accept NaN. + if (detail::impl::constexpr_isnan(raw)) + { + if (std::is_constant_evaluated()) + { + throw std::domain_error("bounded_float NaN value"); + } + else + { + BOOST_SAFE_NUMBERS_THROW_EXCEPTION(std::domain_error, "bounded_float NaN value"); + } + } + + constexpr auto min_raw {static_cast(detail::float_raw_value(Min))}; + constexpr auto max_raw {static_cast(detail::float_raw_value(Max))}; + + if (raw < min_raw || raw > max_raw) + { + if (std::is_constant_evaluated()) + { + throw std::domain_error("bounded_float value out of range"); + } + else + { + BOOST_SAFE_NUMBERS_THROW_EXCEPTION(std::domain_error, "bounded_float value out of range"); + } + } + + basis_ = val; + } + + explicit constexpr bounded_float(const underlying_type val) : bounded_float{basis_type{val}} {} + + template + requires (detail::is_compatible_float_type) + [[nodiscard]] explicit constexpr operator OtherBasis() const + { + const auto raw {static_cast(basis_)}; + + if constexpr (sizeof(OtherBasis) < sizeof(underlying_type)) + { + const auto result {static_cast(raw)}; + + if (detail::impl::constexpr_isinf(result) && !detail::impl::constexpr_isinf(raw)) + { + if (std::is_constant_evaluated()) + { + throw std::overflow_error("bounded_float narrowing conversion overflow"); + } + else + { + BOOST_SAFE_NUMBERS_THROW_EXCEPTION(std::overflow_error, "bounded_float narrowing conversion overflow"); + } + } + + return result; + } + else + { + return static_cast(raw); + } + } + + template + [[nodiscard]] explicit constexpr operator bounded_float() const + { + using target_basis = typename bounded_float::basis_type; + using target_underlying = detail::underlying_type_t; + const auto raw {static_cast(basis_)}; + return bounded_float{target_basis{static_cast(raw)}}; + } + + [[nodiscard]] constexpr auto to_basis() const noexcept -> basis_type { return basis_; } + + [[nodiscard]] friend constexpr auto operator==(bounded_float lhs, bounded_float rhs) noexcept -> bool = default; + + [[nodiscard]] friend constexpr auto operator<=>(bounded_float lhs, bounded_float rhs) noexcept + -> std::partial_ordering = default; + + constexpr auto operator+=(bounded_float rhs) -> bounded_float&; + + constexpr auto operator-=(bounded_float rhs) -> bounded_float&; + + constexpr auto operator*=(bounded_float rhs) -> bounded_float&; + + constexpr auto operator/=(bounded_float rhs) -> bounded_float&; + + constexpr auto operator%=(bounded_float rhs) -> bounded_float&; +}; + +// ------------------------------ +// Free-function arithmetic +// ------------------------------ +// +// Each operator delegates to the underlying float_basis arithmetic, which already +// throws on IEEE 754 issues (overflow, underflow, NaN, invalid_op, divide_by_zero). +// The bounded_float constructor then re-validates the result against [Min, Max]. + +template +[[nodiscard]] constexpr auto operator+(const bounded_float lhs, + const bounded_float rhs) -> bounded_float +{ + using basis = typename bounded_float::basis_type; + using underlying = detail::underlying_type_t; + const basis lhs_b {static_cast(lhs)}; + const basis rhs_b {static_cast(rhs)}; + return bounded_float{lhs_b + rhs_b}; +} + +template +[[nodiscard]] constexpr auto operator-(const bounded_float lhs, + const bounded_float rhs) -> bounded_float +{ + using basis = typename bounded_float::basis_type; + using underlying = detail::underlying_type_t; + const basis lhs_b {static_cast(lhs)}; + const basis rhs_b {static_cast(rhs)}; + return bounded_float{lhs_b - rhs_b}; +} + +template +[[nodiscard]] constexpr auto operator*(const bounded_float lhs, + const bounded_float rhs) -> bounded_float +{ + using basis = typename bounded_float::basis_type; + using underlying = detail::underlying_type_t; + const basis lhs_b {static_cast(lhs)}; + const basis rhs_b {static_cast(rhs)}; + return bounded_float{lhs_b * rhs_b}; +} + +template +[[nodiscard]] constexpr auto operator/(const bounded_float lhs, + const bounded_float rhs) -> bounded_float +{ + using basis = typename bounded_float::basis_type; + using underlying = detail::underlying_type_t; + const basis lhs_b {static_cast(lhs)}; + const basis rhs_b {static_cast(rhs)}; + return bounded_float{lhs_b / rhs_b}; +} + +template +[[nodiscard]] constexpr auto operator%(const bounded_float lhs, + const bounded_float rhs) -> bounded_float +{ + using basis = typename bounded_float::basis_type; + using underlying = detail::underlying_type_t; + const basis lhs_b {static_cast(lhs)}; + const basis rhs_b {static_cast(rhs)}; + return bounded_float{lhs_b % rhs_b}; +} + +// ------------------------------ +// Compound assignment +// ------------------------------ + +template + requires (detail::valid_float_bound && + detail::valid_float_bound && + std::is_same_v && + detail::float_raw_value(Min) == detail::float_raw_value(Min) && + detail::float_raw_value(Max) == detail::float_raw_value(Max) && + detail::float_raw_value(Max) > detail::float_raw_value(Min)) +constexpr auto bounded_float::operator+=(bounded_float rhs) -> bounded_float& +{ + *this = *this + rhs; + return *this; +} + +template + requires (detail::valid_float_bound && + detail::valid_float_bound && + std::is_same_v && + detail::float_raw_value(Min) == detail::float_raw_value(Min) && + detail::float_raw_value(Max) == detail::float_raw_value(Max) && + detail::float_raw_value(Max) > detail::float_raw_value(Min)) +constexpr auto bounded_float::operator-=(bounded_float rhs) -> bounded_float& +{ + *this = *this - rhs; + return *this; +} + +template + requires (detail::valid_float_bound && + detail::valid_float_bound && + std::is_same_v && + detail::float_raw_value(Min) == detail::float_raw_value(Min) && + detail::float_raw_value(Max) == detail::float_raw_value(Max) && + detail::float_raw_value(Max) > detail::float_raw_value(Min)) +constexpr auto bounded_float::operator*=(bounded_float rhs) -> bounded_float& +{ + *this = *this * rhs; + return *this; +} + +template + requires (detail::valid_float_bound && + detail::valid_float_bound && + std::is_same_v && + detail::float_raw_value(Min) == detail::float_raw_value(Min) && + detail::float_raw_value(Max) == detail::float_raw_value(Max) && + detail::float_raw_value(Max) > detail::float_raw_value(Min)) +constexpr auto bounded_float::operator/=(bounded_float rhs) -> bounded_float& +{ + *this = *this / rhs; + return *this; +} + +template + requires (detail::valid_float_bound && + detail::valid_float_bound && + std::is_same_v && + detail::float_raw_value(Min) == detail::float_raw_value(Min) && + detail::float_raw_value(Max) == detail::float_raw_value(Max) && + detail::float_raw_value(Max) > detail::float_raw_value(Min)) +constexpr auto bounded_float::operator%=(bounded_float rhs) -> bounded_float& +{ + *this = *this % rhs; + return *this; +} + +} // namespace boost::safe_numbers + +// Mixed-bounds blocking for bounded_float + +#define BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_FLOAT_OP(OP_NAME, OP_SYMBOL) \ +template \ + requires (LHSMin != RHSMin || LHSMax != RHSMax) \ +constexpr auto OP_SYMBOL(const boost::safe_numbers::bounded_float, \ + const boost::safe_numbers::bounded_float) \ +{ \ + static_assert(boost::safe_numbers::detail::dependent_false< \ + boost::safe_numbers::bounded_float, \ + boost::safe_numbers::bounded_float>, \ + "Can not perform " OP_NAME " between bounded_float types with different bounds. " \ + "Both operands must have the same Min and Max."); \ + \ + return boost::safe_numbers::bounded_float( \ + typename boost::safe_numbers::bounded_float::basis_type{0}); \ +} + +namespace boost::safe_numbers { + +BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_FLOAT_OP("comparison", operator<=>) +BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_FLOAT_OP("equality", operator==) +BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_FLOAT_OP("addition", operator+) +BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_FLOAT_OP("subtraction", operator-) +BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_FLOAT_OP("multiplication", operator*) +BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_FLOAT_OP("division", operator/) +BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_FLOAT_OP("modulo", operator%) + +} // namespace boost::safe_numbers + +#undef BOOST_SAFE_NUMBERS_DEFINE_MIXED_BOUNDED_FLOAT_OP + +#endif // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +#endif // BOOST_SAFE_NUMBERS_BOUNDED_FLOATS_HPP diff --git a/include/boost/safe_numbers/detail/config.hpp b/include/boost/safe_numbers/detail/config.hpp index c584aa8..cf03d48 100644 --- a/include/boost/safe_numbers/detail/config.hpp +++ b/include/boost/safe_numbers/detail/config.hpp @@ -56,6 +56,15 @@ # define BOOST_SAFE_NUMBERS_UNREACHABLE std::abort() #endif +// bounded_float requires floating-point non-type template parameters +// Checking defined(__cpp_nontype_template_args) && __cpp_nontype_template_args >= 201911L +// ends up removing compilers that have support, such as clang 18 +#if (defined(__clang__) && __clang_major__ >= 18) || (defined(__GNUC__) && __GNUC__ >= 11) || (defined(_MSC_VER) && _MSC_VER >= 1943) || (defined(__cpp_nontype_template_args) && __cpp_nontype_template_args >= 201911L) +# define BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT 1 +#else +# define BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT 0 +#endif + namespace boost::safe_numbers::detail { // Workaround for static_assert(false, ...) in if constexpr branches. diff --git a/include/boost/safe_numbers/detail/type_traits.hpp b/include/boost/safe_numbers/detail/type_traits.hpp index 05251e8..d9ebb9e 100644 --- a/include/boost/safe_numbers/detail/type_traits.hpp +++ b/include/boost/safe_numbers/detail/type_traits.hpp @@ -5,6 +5,7 @@ #ifndef BOOST_SAFE_NUMBERS_DETAIL_TYPE_TRAITS_HPP #define BOOST_SAFE_NUMBERS_DETAIL_TYPE_TRAITS_HPP +#include #include #ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE @@ -68,12 +69,18 @@ struct is_unsigned_library_type : std::false_type {}; template struct is_signed_library_type : std::false_type {}; +template +struct is_float_library_type : std::false_type {}; + template struct is_unsigned_library_type> : std::true_type {}; template struct is_signed_library_type> : std::true_type {}; +template +struct is_float_library_type> : std::true_type {}; + } // namespace impl template @@ -82,6 +89,9 @@ inline constexpr bool is_unsigned_library_type_v = impl::is_unsigned_library_typ template inline constexpr bool is_signed_library_type_v = impl::is_signed_library_type::value; +template +inline constexpr bool is_float_library_type_v = impl::is_float_library_type::value; + // underlying type trait (base + unsigned_integer_basis specialization) namespace impl { @@ -157,6 +167,27 @@ constexpr auto signed_raw_value(T val) noexcept } } +// valid_float_bound concept + +template +concept valid_float_bound = is_compatible_float_type || is_float_library_type_v; + +// float_raw_value function + +template + requires valid_float_bound +constexpr auto float_raw_value(T val) noexcept +{ + if constexpr (is_float_library_type_v) + { + return static_cast>(val); + } + else + { + return val; + } +} + } // namespace boost::safe_numbers::detail // Constrained forward declaration of bounded_uint @@ -181,6 +212,24 @@ class bounded_int; } // namespace boost::safe_numbers +// Constrained forward declaration of bounded_float (requires float NTTP support) +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +namespace boost::safe_numbers { + +template + requires (detail::valid_float_bound && + detail::valid_float_bound && + std::is_same_v && + detail::float_raw_value(Min) == detail::float_raw_value(Min) && + detail::float_raw_value(Max) == detail::float_raw_value(Max) && + detail::float_raw_value(Max) > detail::float_raw_value(Min)) +class bounded_float; + +} // namespace boost::safe_numbers + +#endif // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + // bounded_uint specialization of is_unsigned_library_type namespace boost::safe_numbers::detail::impl { @@ -197,6 +246,18 @@ struct is_signed_library_type> : std::true_type {}; } // namespace boost::safe_numbers::detail::impl +// bounded_float specialization of is_float_library_type +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +namespace boost::safe_numbers::detail::impl { + +template +struct is_float_library_type> : std::true_type {}; + +} // namespace boost::safe_numbers::detail::impl + +#endif // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + // is_bounded_type trait namespace boost::safe_numbers::detail { @@ -211,6 +272,11 @@ struct is_bounded_type> : std::true_type {}; template struct is_bounded_type> : std::true_type {}; +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT +template +struct is_bounded_type> : std::true_type {}; +#endif + } // namespace impl template @@ -238,6 +304,11 @@ struct is_library_type> : std::true_type {}; template struct is_library_type> : std::true_type {}; +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT +template +struct is_library_type> : std::true_type {}; +#endif + template struct is_integral_library_type : std::false_type {}; @@ -292,6 +363,14 @@ struct underlying> using type = typename underlying::basis_type>::type; }; +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT +template +struct underlying> +{ + using type = typename underlying::basis_type>::type; +}; +#endif + } // namespace impl // Promotes an unsigned integer to the next higher type diff --git a/include/boost/safe_numbers/limits.hpp b/include/boost/safe_numbers/limits.hpp index 065e704..8670e42 100644 --- a/include/boost/safe_numbers/limits.hpp +++ b/include/boost/safe_numbers/limits.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #ifndef BOOST_SAFE_NUMBERS_BUILD_MODULE @@ -213,6 +214,56 @@ class numeric_limits> static constexpr type denorm_min() { return min(); } }; +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +template +class numeric_limits> +{ + using type = boost::safe_numbers::bounded_float; + using underlying_type = boost::safe_numbers::detail::underlying_type_t; + +public: + static constexpr bool is_specialized = std::numeric_limits::is_specialized; + static constexpr bool is_signed = std::numeric_limits::is_signed; + static constexpr bool is_integer = false; + static constexpr bool is_exact = false; + static constexpr bool has_infinity = false; + static constexpr bool has_quiet_NaN = false; + static constexpr bool has_signaling_NaN = false; + + #if ((!defined(_MSC_VER) && (__cplusplus <= 202002L)) || (defined(_MSC_VER) && (_MSVC_LANG <= 202002L))) + static constexpr std::float_denorm_style has_denorm = std::numeric_limits::has_denorm; + static constexpr bool has_denorm_loss = std::numeric_limits::has_denorm_loss; + #endif + + static constexpr std::float_round_style round_style = std::numeric_limits::round_style; + static constexpr bool is_iec559 = false; + static constexpr bool is_bounded = true; + static constexpr bool is_modulo = false; + static constexpr int digits = std::numeric_limits::digits; + static constexpr int digits10 = std::numeric_limits::digits10; + static constexpr int max_digits10 = std::numeric_limits::max_digits10; + static constexpr int radix = std::numeric_limits::radix; + static constexpr int min_exponent = std::numeric_limits::min_exponent; + static constexpr int min_exponent10 = std::numeric_limits::min_exponent10; + static constexpr int max_exponent = std::numeric_limits::max_exponent; + static constexpr int max_exponent10 = std::numeric_limits::max_exponent10; + static constexpr bool traps = std::numeric_limits::traps; + static constexpr bool tinyness_before = std::numeric_limits::tinyness_before; + + static constexpr type min() { return type{static_cast(boost::safe_numbers::detail::float_raw_value(Min))}; } + static constexpr type max() { return type{static_cast(boost::safe_numbers::detail::float_raw_value(Max))}; } + static constexpr type lowest() { return min(); } + static constexpr type epsilon() { return min(); } + static constexpr type round_error() { return min(); } + static constexpr type infinity() { return min(); } + static constexpr type quiet_NaN() { return min(); } + static constexpr type signaling_NaN() { return min(); } + static constexpr type denorm_min() { return min(); } +}; + +#endif // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + #ifdef __clang__ # pragma clang diagnostic pop #endif diff --git a/test/Jamfile b/test/Jamfile index 3c5364f..c0ae7e9 100644 --- a/test/Jamfile +++ b/test/Jamfile @@ -162,6 +162,18 @@ run test_signed_bounded_modulo.cpp ; run test_signed_bounded_conversions.cpp ; run test_signed_bounded_unary.cpp ; compile-fail compile_fail_bounded_int_mixed_ops.cpp ; +run test_bounded_float_construction.cpp ; +run test_bounded_float_addition.cpp ; +run test_bounded_float_subtraction.cpp ; +run test_bounded_float_multiplication.cpp ; +run test_bounded_float_division.cpp ; +run test_bounded_float_modulo.cpp ; +run test_bounded_float_conversions.cpp ; +run test_bounded_float_streaming.cpp ; +run test_bounded_float_std_format.cpp ; +run test_bounded_float_fmt_format.cpp ; +run test_bounded_float_limits.cpp ; +compile-fail compile_fail_bounded_float_mixed_ops.cpp ; # Signed free function tests run test_signed_charconv.cpp ; diff --git a/test/compile_fail_bounded_float_mixed_ops.cpp b/test/compile_fail_bounded_float_mixed_ops.cpp new file mode 100644 index 0000000..dc964ee --- /dev/null +++ b/test/compile_fail_bounded_float_mixed_ops.cpp @@ -0,0 +1,34 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +#include +#include + +using namespace boost::safe_numbers; + +int main() +{ + bounded_float<-1.0f, 1.0f> a {f32{0.5f}}; + bounded_float<-2.0f, 2.0f> b {f32{0.5f}}; + + auto c = a + b; + (void)c; + + return 0; +} + +#else + +// Force a compile failure on compilers without float NTTP support +int main() +{ + static_assert(boost::safe_numbers::detail::dependent_false, "bounded_float requires float NTTP support"); + return 1; +} + +#endif diff --git a/test/test_bounded_float_addition.cpp b/test/test_bounded_float_addition.cpp new file mode 100644 index 0000000..35fd34f --- /dev/null +++ b/test/test_bounded_float_addition.cpp @@ -0,0 +1,75 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include + +#endif + +#include + +using namespace boost::safe_numbers; + +void test_addition_in_bounds() +{ + const bounded_float<-10.0f, 10.0f> a {f32{1.0f}}; + const bounded_float<-10.0f, 10.0f> b {f32{2.0f}}; + const auto r {a + b}; + const bounded_float<-10.0f, 10.0f> expected {f32{3.0f}}; + BOOST_TEST(r == expected); + + const bounded_float<0.0, 1000.0> c {f64{500.0}}; + const bounded_float<0.0, 1000.0> d {f64{250.0}}; + const auto r2 {c + d}; + const bounded_float<0.0, 1000.0> expected2 {f64{750.0}}; + BOOST_TEST(r2 == expected2); +} + +void test_addition_post_op_out_of_range() +{ + const bounded_float<-1.0f, 1.0f> a {f32{0.6f}}; + const bounded_float<-1.0f, 1.0f> b {f32{0.6f}}; + BOOST_TEST_THROWS((void)(a + b), std::domain_error); + + const bounded_float<-1.0f, 1.0f> c {f32{-0.6f}}; + const bounded_float<-1.0f, 1.0f> d {f32{-0.6f}}; + BOOST_TEST_THROWS((void)(c + d), std::domain_error); +} + +void test_addition_compound_assignment() +{ + bounded_float<-10.0f, 10.0f> a {f32{1.0f}}; + const bounded_float<-10.0f, 10.0f> b {f32{2.0f}}; + a += b; + const bounded_float<-10.0f, 10.0f> expected {f32{3.0f}}; + BOOST_TEST(a == expected); + + BOOST_TEST_THROWS((void)(a += bounded_float<-10.0f, 10.0f>{f32{8.0f}}), std::domain_error); +} + +int main() +{ + test_addition_in_bounds(); + test_addition_post_op_out_of_range(); + test_addition_compound_assignment(); + + return boost::report_errors(); +} + +#else // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +int main() { return 0; } + +#endif diff --git a/test/test_bounded_float_construction.cpp b/test/test_bounded_float_construction.cpp new file mode 100644 index 0000000..cb48551 --- /dev/null +++ b/test/test_bounded_float_construction.cpp @@ -0,0 +1,203 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include + +#endif + +#include +#include +#include +#include + +using namespace boost::safe_numbers; + +// ----------------------------------------------- +// basis_type selection follows the bound's literal type +// ----------------------------------------------- + +static_assert(std::is_same_v::basis_type, f32>); +static_assert(std::is_same_v::basis_type, f32>); +static_assert(std::is_same_v::basis_type, f64>); +static_assert(std::is_same_v::basis_type, f64>); +static_assert(std::is_same_v::basis_type, f64>); + +// ----------------------------------------------- +// sizeof: bounded_float should match its basis_type +// ----------------------------------------------- + +static_assert(sizeof(bounded_float<-1.0f, 1.0f>) == sizeof(f32)); +static_assert(sizeof(bounded_float<-1.0, 1.0>) == sizeof(f64)); +static_assert(sizeof(bounded_float<-1.0e100, 1.0e100>) == sizeof(f64)); + +// ----------------------------------------------- +// No default constructor (mirrors bounded_int) +// ----------------------------------------------- + +static_assert(!std::is_default_constructible_v>); +static_assert(!std::is_default_constructible_v>); + +// ----------------------------------------------- +// Valid construction at boundaries +// ----------------------------------------------- + +void test_f32_boundary_construction() +{ + constexpr bounded_float<-1.0f, 1.0f> a {f32{-1.0f}}; + constexpr bounded_float<-1.0f, 1.0f> b {f32{1.0f}}; + constexpr bounded_float<-1.0f, 1.0f> c {f32{0.0f}}; + (void)a; + (void)b; + (void)c; + + constexpr bounded_float<0.0f, 100.0f> d {f32{0.0f}}; + constexpr bounded_float<0.0f, 100.0f> e {f32{100.0f}}; + constexpr bounded_float<0.0f, 100.0f> f {f32{50.5f}}; + (void)d; + (void)e; + (void)f; +} + +void test_f64_boundary_construction() +{ + constexpr bounded_float<-1.0e100, 1.0e100> a {f64{-1.0e100}}; + constexpr bounded_float<-1.0e100, 1.0e100> b {f64{1.0e100}}; + (void)a; + (void)b; +} + +// ----------------------------------------------- +// Out-of-range construction throws std::domain_error +// ----------------------------------------------- + +void test_out_of_range() +{ + BOOST_TEST_THROWS((bounded_float<-1.0f, 1.0f>{f32{-1.5f}}), std::domain_error); + BOOST_TEST_THROWS((bounded_float<-1.0f, 1.0f>{f32{1.5f}}), std::domain_error); + BOOST_TEST_THROWS((bounded_float<-1.0f, 1.0f>{f32{2.0f}}), std::domain_error); + BOOST_TEST_THROWS((bounded_float<-1.0f, 1.0f>{f32{-2.0f}}), std::domain_error); + + BOOST_TEST_THROWS((bounded_float<0.0f, 100.0f>{f32{-0.1f}}), std::domain_error); + BOOST_TEST_THROWS((bounded_float<0.0f, 100.0f>{f32{100.5f}}), std::domain_error); +} + +// ----------------------------------------------- +// NaN construction throws std::domain_error +// ----------------------------------------------- + +void test_nan_rejected() +{ + const auto qnan_f {std::numeric_limits::quiet_NaN()}; + const auto qnan_d {std::numeric_limits::quiet_NaN()}; + const auto snan_f {std::numeric_limits::signaling_NaN()}; + const auto snan_d {std::numeric_limits::signaling_NaN()}; + + BOOST_TEST_THROWS((bounded_float<-1.0f, 1.0f>{f32{qnan_f}}), std::domain_error); + BOOST_TEST_THROWS((bounded_float<-1.0f, 1.0f>{f32{snan_f}}), std::domain_error); + BOOST_TEST_THROWS((bounded_float<-1.0e100, 1.0e100>{f64{qnan_d}}), std::domain_error); + BOOST_TEST_THROWS((bounded_float<-1.0e100, 1.0e100>{f64{snan_d}}), std::domain_error); + + BOOST_TEST_THROWS((bounded_float<-1.0f, 1.0f>{qnan_f}), std::domain_error); + BOOST_TEST_THROWS((bounded_float<-1.0e100, 1.0e100>{qnan_d}), std::domain_error); +} + +// ----------------------------------------------- +// Infinity outside finite bounds throws +// ----------------------------------------------- + +void test_infinity_outside_finite_bounds() +{ + const auto pinf_f {std::numeric_limits::infinity()}; + const auto ninf_f {-std::numeric_limits::infinity()}; + + BOOST_TEST_THROWS((bounded_float<-1.0f, 1.0f>{f32{pinf_f}}), std::domain_error); + BOOST_TEST_THROWS((bounded_float<-1.0f, 1.0f>{f32{ninf_f}}), std::domain_error); +} + +// ----------------------------------------------- +// Construction from underlying float / double +// ----------------------------------------------- + +void test_construction_from_underlying() +{ + bounded_float<-1.0f, 1.0f> a {0.5f}; + const bounded_float<-1.0f, 1.0f> expected_a {f32{0.5f}}; + BOOST_TEST(a == expected_a); + + bounded_float<-1.0e100, 1.0e100> b {1.0e50}; + const bounded_float<-1.0e100, 1.0e100> expected_b {f64{1.0e50}}; + BOOST_TEST(b == expected_b); +} + +// ----------------------------------------------- +// Constexpr construction and comparisons +// ----------------------------------------------- + +void test_constexpr_construction() +{ + constexpr bounded_float<-1.0f, 1.0f> a {f32{-0.5f}}; + constexpr bounded_float<-1.0f, 1.0f> b {f32{0.5f}}; + static_assert(a < b); + static_assert(a == a); + static_assert(a != b); + + constexpr bounded_float<-1.0e100, 1.0e100> c {f64{-1.0e50}}; + constexpr bounded_float<-1.0e100, 1.0e100> d {f64{1.0e50}}; + static_assert(c < d); +} + +// ----------------------------------------------- +// Comparison operators +// ----------------------------------------------- + +void test_comparisons() +{ + constexpr bounded_float<-1.0f, 1.0f> a {f32{-0.5f}}; + constexpr bounded_float<-1.0f, 1.0f> b {f32{0.0f}}; + constexpr bounded_float<-1.0f, 1.0f> c {f32{0.5f}}; + constexpr bounded_float<-1.0f, 1.0f> d {f32{0.5f}}; + + BOOST_TEST(a < b); + BOOST_TEST(b < c); + BOOST_TEST(a < c); + BOOST_TEST(c == d); + BOOST_TEST(c != a); + BOOST_TEST(c > a); + BOOST_TEST(a <= b); + BOOST_TEST(c >= b); + BOOST_TEST(c <= d); + BOOST_TEST(c >= d); +} + +int main() +{ + test_f32_boundary_construction(); + test_f64_boundary_construction(); + test_out_of_range(); + test_nan_rejected(); + test_infinity_outside_finite_bounds(); + test_construction_from_underlying(); + test_constexpr_construction(); + test_comparisons(); + + return boost::report_errors(); +} + +#else // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +int main() { return 0; } + +#endif diff --git a/test/test_bounded_float_conversions.cpp b/test/test_bounded_float_conversions.cpp new file mode 100644 index 0000000..82a83c5 --- /dev/null +++ b/test/test_bounded_float_conversions.cpp @@ -0,0 +1,102 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include + +#endif + +#include +#include +#include + +using namespace boost::safe_numbers; + +// Bit-pattern equality avoids -Wfloat-equal in BOOST_TEST_EQ on raw floats. + +void test_conversion_to_underlying_float() +{ + const bounded_float<-1.0f, 1.0f> a {f32{0.5f}}; + const auto raw {static_cast(a)}; + BOOST_TEST_EQ(std::bit_cast(raw), std::bit_cast(0.5f)); +} + +void test_conversion_to_underlying_double() +{ + const bounded_float<-1.0e100, 1.0e100> a {f64{1.0e50}}; + const auto raw {static_cast(a)}; + BOOST_TEST_EQ(std::bit_cast(raw), std::bit_cast(1.0e50)); +} + +void test_widening_conversion() +{ + const bounded_float<-1.0f, 1.0f> a {f32{0.5f}}; + const auto widened {static_cast(a)}; + BOOST_TEST_EQ(std::bit_cast(widened), std::bit_cast(0.5)); +} + +void test_narrowing_conversion_in_range() +{ + const bounded_float<-1.0e10, 1.0e10> a {f64{1.0e5}}; + const auto narrowed {static_cast(a)}; + BOOST_TEST_EQ(std::bit_cast(narrowed), std::bit_cast(1.0e5f)); +} + +void test_narrowing_conversion_overflow() +{ + // 1e40 is within double's range but greater than FLT_MAX (~3.4e38) + const bounded_float<-1.0e100, 1.0e100> a {f64{1.0e40}}; + BOOST_TEST_THROWS((void)static_cast(a), std::overflow_error); +} + +void test_to_basis() +{ + const bounded_float<-1.0f, 1.0f> a {f32{0.5f}}; + const f32 b {a.to_basis()}; + const f32 expected {0.5f}; + BOOST_TEST(b == expected); +} + +void test_conversion_between_bounded_floats() +{ + const bounded_float<-1.0f, 1.0f> a {f32{0.5f}}; + const auto b {static_cast>(a)}; + const bounded_float<-2.0f, 2.0f> expected {f32{0.5f}}; + BOOST_TEST(b == expected); + + // Conversion that puts value out of new bounds throws + const bounded_float<-1.0f, 1.0f> c {f32{0.75f}}; + using narrow_t = bounded_float<-0.5f, 0.5f>; + BOOST_TEST_THROWS((void)static_cast(c), std::domain_error); +} + +int main() +{ + test_conversion_to_underlying_float(); + test_conversion_to_underlying_double(); + test_widening_conversion(); + test_narrowing_conversion_in_range(); + test_narrowing_conversion_overflow(); + test_to_basis(); + test_conversion_between_bounded_floats(); + + return boost::report_errors(); +} + +#else // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +int main() { return 0; } + +#endif diff --git a/test/test_bounded_float_division.cpp b/test/test_bounded_float_division.cpp new file mode 100644 index 0000000..f434270 --- /dev/null +++ b/test/test_bounded_float_division.cpp @@ -0,0 +1,71 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include + +#endif + +#include + +using namespace boost::safe_numbers; + +void test_division_in_bounds() +{ + const bounded_float<-100.0f, 100.0f> a {f32{20.0f}}; + const bounded_float<-100.0f, 100.0f> b {f32{4.0f}}; + const auto r {a / b}; + const bounded_float<-100.0f, 100.0f> expected {f32{5.0f}}; + BOOST_TEST(r == expected); +} + +void test_division_by_zero() +{ + const bounded_float<-100.0f, 100.0f> a {f32{20.0f}}; + const bounded_float<-100.0f, 100.0f> b {f32{0.0f}}; + BOOST_TEST_THROWS((void)(a / b), std::domain_error); +} + +void test_division_post_op_out_of_range() +{ + const bounded_float<-1.0f, 1.0f> a {f32{0.5f}}; + const bounded_float<-1.0f, 1.0f> b {f32{0.25f}}; + BOOST_TEST_THROWS((void)(a / b), std::domain_error); +} + +void test_division_compound_assignment() +{ + bounded_float<-100.0f, 100.0f> a {f32{20.0f}}; + const bounded_float<-100.0f, 100.0f> b {f32{4.0f}}; + a /= b; + const bounded_float<-100.0f, 100.0f> expected {f32{5.0f}}; + BOOST_TEST(a == expected); +} + +int main() +{ + test_division_in_bounds(); + test_division_by_zero(); + test_division_post_op_out_of_range(); + test_division_compound_assignment(); + + return boost::report_errors(); +} + +#else // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +int main() { return 0; } + +#endif diff --git a/test/test_bounded_float_fmt_format.cpp b/test/test_bounded_float_fmt_format.cpp new file mode 100644 index 0000000..152add3 --- /dev/null +++ b/test/test_bounded_float_fmt_format.cpp @@ -0,0 +1,69 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wfloat-equal" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +# pragma GCC diagnostic ignored "-Wfloat-equal" +# pragma GCC diagnostic ignored "-Wsign-conversion" +# pragma GCC diagnostic ignored "-Wconversion" +#endif + +#define FMT_HEADER_ONLY + +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT && __has_include() + +#include +#include +#include +#include + +using namespace boost::safe_numbers; + +void test_basic() +{ + const bounded_float<-100.0f, 100.0f> x {f32{42.5f}}; + + BOOST_TEST_CSTR_EQ(fmt::format("{}", x).c_str(), "42.5"); + BOOST_TEST_CSTR_EQ(fmt::format("{:.2f}", x).c_str(), "42.50"); + BOOST_TEST_CSTR_EQ(fmt::format("{:.2e}", x).c_str(), "4.25e+01"); + BOOST_TEST_CSTR_EQ(fmt::format("{:+.1f}", x).c_str(), "+42.5"); + BOOST_TEST_CSTR_EQ(fmt::format("{:>10.2f}", x).c_str(), " 42.50"); +} + +void test_negative() +{ + const bounded_float<-100.0, 100.0> x {f64{-42.5}}; + + BOOST_TEST_CSTR_EQ(fmt::format("{}", x).c_str(), "-42.5"); + BOOST_TEST_CSTR_EQ(fmt::format("{:.2f}", x).c_str(), "-42.50"); + BOOST_TEST_CSTR_EQ(fmt::format("{:+.1f}", x).c_str(), "-42.5"); +} + +void test_zero() +{ + const bounded_float<-1.0f, 1.0f> x {f32{0.0f}}; + + BOOST_TEST_CSTR_EQ(fmt::format("{}", x).c_str(), "0"); + BOOST_TEST_CSTR_EQ(fmt::format("{:.2f}", x).c_str(), "0.00"); +} + +int main() +{ + test_basic(); + test_negative(); + test_zero(); + + return boost::report_errors(); +} + +#else + +int main() { return 0; } + +#endif diff --git a/test/test_bounded_float_limits.cpp b/test/test_bounded_float_limits.cpp new file mode 100644 index 0000000..0efb7dd --- /dev/null +++ b/test/test_bounded_float_limits.cpp @@ -0,0 +1,80 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include +#include + +#endif + +#include +#include + +using namespace boost::safe_numbers; + +// ----------------------------------------------- +// Compile-time properties +// ----------------------------------------------- + +using BFLow = bounded_float<-1.0f, 1.0f>; +using BFWide = bounded_float<-1.0e100, 1.0e100>; + +static_assert(std::numeric_limits::is_specialized); +static_assert(!std::numeric_limits::is_iec559); +static_assert(!std::numeric_limits::has_infinity); +static_assert(!std::numeric_limits::has_quiet_NaN); +static_assert(!std::numeric_limits::has_signaling_NaN); +static_assert(!std::numeric_limits::is_integer); +static_assert(!std::numeric_limits::is_exact); +static_assert(std::numeric_limits::is_bounded); +static_assert(std::numeric_limits::is_signed); + +static_assert(std::numeric_limits::digits == std::numeric_limits::digits); +static_assert(std::numeric_limits::digits10 == std::numeric_limits::digits10); +static_assert(std::numeric_limits::radix == std::numeric_limits::radix); + +static_assert(std::numeric_limits::digits == std::numeric_limits::digits); +static_assert(std::numeric_limits::digits10 == std::numeric_limits::digits10); + +// ----------------------------------------------- +// min() / max() / lowest() +// ----------------------------------------------- + +void test_min_max() +{ + constexpr auto min_val {std::numeric_limits::min()}; + constexpr auto max_val {std::numeric_limits::max()}; + constexpr auto lowest_val {std::numeric_limits::lowest()}; + + const BFLow expected_min {f32{-1.0f}}; + const BFLow expected_max {f32{1.0f}}; + + BOOST_TEST(min_val == expected_min); + BOOST_TEST(max_val == expected_max); + BOOST_TEST(lowest_val == expected_min); +} + +int main() +{ + test_min_max(); + + return boost::report_errors(); +} + +#else // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +int main() { return 0; } + +#endif diff --git a/test/test_bounded_float_modulo.cpp b/test/test_bounded_float_modulo.cpp new file mode 100644 index 0000000..d700b02 --- /dev/null +++ b/test/test_bounded_float_modulo.cpp @@ -0,0 +1,71 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include + +#endif + +#include + +using namespace boost::safe_numbers; + +void test_modulo_in_bounds() +{ + const bounded_float<-100.0f, 100.0f> a {f32{7.5f}}; + const bounded_float<-100.0f, 100.0f> b {f32{3.0f}}; + const auto r {a % b}; + const bounded_float<-100.0f, 100.0f> expected {f32{1.5f}}; + BOOST_TEST(r == expected); +} + +void test_modulo_by_zero() +{ + const bounded_float<-100.0f, 100.0f> a {f32{7.5f}}; + const bounded_float<-100.0f, 100.0f> b {f32{0.0f}}; + BOOST_TEST_THROWS((void)(a % b), std::domain_error); +} + +void test_modulo_post_op_out_of_range() +{ + const bounded_float<5.0f, 10.0f> a {f32{7.5f}}; + const bounded_float<5.0f, 10.0f> b {f32{6.0f}}; + BOOST_TEST_THROWS((void)(a % b), std::domain_error); +} + +void test_modulo_compound_assignment() +{ + bounded_float<-100.0f, 100.0f> a {f32{7.5f}}; + const bounded_float<-100.0f, 100.0f> b {f32{3.0f}}; + a %= b; + const bounded_float<-100.0f, 100.0f> expected {f32{1.5f}}; + BOOST_TEST(a == expected); +} + +int main() +{ + test_modulo_in_bounds(); + test_modulo_by_zero(); + test_modulo_post_op_out_of_range(); + test_modulo_compound_assignment(); + + return boost::report_errors(); +} + +#else // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +int main() { return 0; } + +#endif diff --git a/test/test_bounded_float_multiplication.cpp b/test/test_bounded_float_multiplication.cpp new file mode 100644 index 0000000..00138b3 --- /dev/null +++ b/test/test_bounded_float_multiplication.cpp @@ -0,0 +1,69 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include + +#endif + +#include + +using namespace boost::safe_numbers; + +void test_multiplication_in_bounds() +{ + const bounded_float<-100.0f, 100.0f> a {f32{4.0f}}; + const bounded_float<-100.0f, 100.0f> b {f32{5.0f}}; + const auto r {a * b}; + const bounded_float<-100.0f, 100.0f> expected {f32{20.0f}}; + BOOST_TEST(r == expected); +} + +void test_multiplication_post_op_out_of_range() +{ + const bounded_float<-1.0f, 1.0f> a {f32{0.5f}}; + const bounded_float<-1.0f, 1.0f> b {f32{0.5f}}; + const auto r {a * b}; + const bounded_float<-1.0f, 1.0f> expected {f32{0.25f}}; + BOOST_TEST(r == expected); + + const bounded_float<-10.0f, 10.0f> c {f32{5.0f}}; + const bounded_float<-10.0f, 10.0f> d {f32{5.0f}}; + BOOST_TEST_THROWS((void)(c * d), std::domain_error); +} + +void test_multiplication_compound_assignment() +{ + bounded_float<-100.0f, 100.0f> a {f32{4.0f}}; + const bounded_float<-100.0f, 100.0f> b {f32{5.0f}}; + a *= b; + const bounded_float<-100.0f, 100.0f> expected {f32{20.0f}}; + BOOST_TEST(a == expected); +} + +int main() +{ + test_multiplication_in_bounds(); + test_multiplication_post_op_out_of_range(); + test_multiplication_compound_assignment(); + + return boost::report_errors(); +} + +#else // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +int main() { return 0; } + +#endif diff --git a/test/test_bounded_float_std_format.cpp b/test/test_bounded_float_std_format.cpp new file mode 100644 index 0000000..1da5f5a --- /dev/null +++ b/test/test_bounded_float_std_format.cpp @@ -0,0 +1,78 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include + +#ifdef BOOST_SAFE_NUMBERS_DETAIL_INT128_HAS_FORMAT + +#include + +#endif + +#endif + +#ifdef BOOST_SAFE_NUMBERS_DETAIL_INT128_HAS_FORMAT + +#include + +using namespace boost::safe_numbers; + +void test_basic() +{ + const bounded_float<-100.0f, 100.0f> x {f32{42.5f}}; + + BOOST_TEST_CSTR_EQ(std::format("{}", x).c_str(), "42.5"); + BOOST_TEST_CSTR_EQ(std::format("{:.2f}", x).c_str(), "42.50"); + BOOST_TEST_CSTR_EQ(std::format("{:.2e}", x).c_str(), "4.25e+01"); + BOOST_TEST_CSTR_EQ(std::format("{:+.1f}", x).c_str(), "+42.5"); + BOOST_TEST_CSTR_EQ(std::format("{:>10.2f}", x).c_str(), " 42.50"); +} + +void test_negative() +{ + const bounded_float<-100.0, 100.0> x {f64{-42.5}}; + + BOOST_TEST_CSTR_EQ(std::format("{}", x).c_str(), "-42.5"); + BOOST_TEST_CSTR_EQ(std::format("{:.2f}", x).c_str(), "-42.50"); + BOOST_TEST_CSTR_EQ(std::format("{:+.1f}", x).c_str(), "-42.5"); +} + +void test_zero() +{ + const bounded_float<-1.0f, 1.0f> x {f32{0.0f}}; + + BOOST_TEST_CSTR_EQ(std::format("{}", x).c_str(), "0"); + BOOST_TEST_CSTR_EQ(std::format("{:.2f}", x).c_str(), "0.00"); +} + +int main() +{ + test_basic(); + test_negative(); + test_zero(); + + return boost::report_errors(); +} + +#else + +int main() { return 0; } + +#endif + +#else // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +int main() { return 0; } + +#endif diff --git a/test/test_bounded_float_streaming.cpp b/test/test_bounded_float_streaming.cpp new file mode 100644 index 0000000..581cc7c --- /dev/null +++ b/test/test_bounded_float_streaming.cpp @@ -0,0 +1,82 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +#include +#include +#include +#include + +using namespace boost::safe_numbers; + +void test_round_trip() +{ + bounded_float<-100.0f, 100.0f> val {f32{0.0f}}; + + std::stringstream in; + in.str("42.5"); + in >> val; + + const bounded_float<-100.0f, 100.0f> expected {f32{42.5f}}; + BOOST_TEST(val == expected); + + std::stringstream out; + out << val; + + BOOST_TEST_CSTR_EQ(out.str().c_str(), "42.5"); +} + +void test_negative_round_trip() +{ + bounded_float<-100.0, 100.0> val {f64{0.0}}; + + std::stringstream in; + in.str("-42.5"); + in >> val; + + const bounded_float<-100.0, 100.0> expected {f64{-42.5}}; + BOOST_TEST(val == expected); + + std::stringstream out; + out << val; + + BOOST_TEST_CSTR_EQ(out.str().c_str(), "-42.5"); +} + +void test_out_of_range_input_throws() +{ + bounded_float<-1.0f, 1.0f> val {f32{0.0f}}; + + std::stringstream in; + in.str("100.0"); + BOOST_TEST_THROWS((in >> val), std::domain_error); +} + +void test_fixed_output() +{ + const bounded_float<-100.0f, 100.0f> val {f32{42.5f}}; + std::stringstream out; + out << std::fixed << std::setprecision(2) << val; + BOOST_TEST_CSTR_EQ(out.str().c_str(), "42.50"); +} + +int main() +{ + test_round_trip(); + test_negative_round_trip(); + test_out_of_range_input_throws(); + test_fixed_output(); + + return boost::report_errors(); +} + +#else // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +int main() { return 0; } + +#endif diff --git a/test/test_bounded_float_subtraction.cpp b/test/test_bounded_float_subtraction.cpp new file mode 100644 index 0000000..93c8559 --- /dev/null +++ b/test/test_bounded_float_subtraction.cpp @@ -0,0 +1,67 @@ +// Copyright 2026 Matt Borland +// Distributed under the Boost Software License, Version 1.0. +// https://www.boost.org/LICENSE_1_0.txt + +#include +#include + +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +#ifdef BOOST_SAFE_NUMBERS_BUILD_MODULE + +import boost.safe_numbers; + +#else + +#include +#include + +#endif + +#include + +using namespace boost::safe_numbers; + +void test_subtraction_in_bounds() +{ + const bounded_float<-10.0f, 10.0f> a {f32{5.0f}}; + const bounded_float<-10.0f, 10.0f> b {f32{2.0f}}; + const auto r {a - b}; + const bounded_float<-10.0f, 10.0f> expected {f32{3.0f}}; + BOOST_TEST(r == expected); +} + +void test_subtraction_post_op_out_of_range() +{ + const bounded_float<-1.0f, 1.0f> a {f32{0.6f}}; + const bounded_float<-1.0f, 1.0f> b {f32{-0.6f}}; + BOOST_TEST_THROWS((void)(a - b), std::domain_error); + + const bounded_float<-1.0f, 1.0f> c {f32{-0.6f}}; + const bounded_float<-1.0f, 1.0f> d {f32{0.6f}}; + BOOST_TEST_THROWS((void)(c - d), std::domain_error); +} + +void test_subtraction_compound_assignment() +{ + bounded_float<-10.0f, 10.0f> a {f32{5.0f}}; + const bounded_float<-10.0f, 10.0f> b {f32{2.0f}}; + a -= b; + const bounded_float<-10.0f, 10.0f> expected {f32{3.0f}}; + BOOST_TEST(a == expected); +} + +int main() +{ + test_subtraction_in_bounds(); + test_subtraction_post_op_out_of_range(); + test_subtraction_compound_assignment(); + + return boost::report_errors(); +} + +#else // BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT + +int main() { return 0; } + +#endif