|
| 1 | +//// |
| 2 | +Copyright 2026 Matt Borland |
| 3 | +Distributed under the Boost Software License, Version 1.0. |
| 4 | +https://www.boost.org/LICENSE_1_0.txt |
| 5 | +//// |
| 6 | + |
| 7 | +[#bounded_float] |
| 8 | + |
| 9 | += Bounded Floating-Point Types |
| 10 | + |
| 11 | +[IMPORTANT] |
| 12 | +.Compiler Requirements |
| 13 | +==== |
| 14 | +`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+*. |
| 15 | +
|
| 16 | +The header `<boost/safe_numbers/bounded_floats.hpp>` 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: |
| 17 | +
|
| 18 | +[source,c++] |
| 19 | +---- |
| 20 | +#include <boost/safe_numbers/bounded_floats.hpp> |
| 21 | +
|
| 22 | +#if BOOST_SAFE_NUMBERS_HAS_BOUNDED_FLOAT |
| 23 | +boost::safe_numbers::bounded_float<-1.0f, 1.0f> x {boost::safe_numbers::f32{0.5f}}; |
| 24 | +#endif |
| 25 | +---- |
| 26 | +
|
| 27 | +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. |
| 28 | +==== |
| 29 | + |
| 30 | +== Description |
| 31 | + |
| 32 | +`bounded_float<Min, Max>` is a compile-time bounded floating-point type that enforces value constraints at both compile time and runtime. |
| 33 | +The bounds `Min` and `Max` are non-type template parameters of type `float` or `double`. |
| 34 | + |
| 35 | +The underlying storage type (`basis_type`) follows the type of the bounds: |
| 36 | + |
| 37 | +|=== |
| 38 | +| Bound Type | Basis Type |
| 39 | + |
| 40 | +| `float` | `f32` |
| 41 | +| `double` | `f64` |
| 42 | +|=== |
| 43 | + |
| 44 | +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>`. |
| 45 | + |
| 46 | +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. |
| 47 | + |
| 48 | +== Synopsis |
| 49 | + |
| 50 | +[source,c++] |
| 51 | +---- |
| 52 | +#include <boost/safe_numbers/bounded_floats.hpp> |
| 53 | +
|
| 54 | +namespace boost::safe_numbers { |
| 55 | +
|
| 56 | +template <auto Min, auto Max> |
| 57 | + requires (valid_float_bound<decltype(Min)> && |
| 58 | + valid_float_bound<decltype(Max)> && |
| 59 | + std::is_same_v<decltype(Min), decltype(Max)> && |
| 60 | + float_raw_value(Min) == float_raw_value(Min) && // not NaN |
| 61 | + float_raw_value(Max) == float_raw_value(Max) && // not NaN |
| 62 | + float_raw_value(Max) > float_raw_value(Min)) |
| 63 | +class bounded_float { |
| 64 | +public: |
| 65 | + using basis_type = /* f32 if decltype(Min) == float, else f64 */; |
| 66 | +
|
| 67 | + // Construction (throws std::domain_error if NaN, signaling-NaN, or out of range) |
| 68 | + explicit constexpr bounded_float(basis_type val); |
| 69 | + explicit constexpr bounded_float(underlying_type val); |
| 70 | +
|
| 71 | + // Conversions to fundamental float / double |
| 72 | + template <CompatibleFloat T> |
| 73 | + explicit constexpr operator T() const; |
| 74 | +
|
| 75 | + template <auto Min2, auto Max2> |
| 76 | + explicit constexpr operator bounded_float<Min2, Max2>() const; |
| 77 | +
|
| 78 | + // Direct accessor for the basis type (since static_cast<basis_type>(...) cannot |
| 79 | + // be used: float_basis has a deleted catch-all constructor that intercepts it). |
| 80 | + constexpr auto to_basis() const noexcept -> basis_type; |
| 81 | +
|
| 82 | + // Comparison (defaulted) |
| 83 | + friend constexpr auto operator==(bounded_float, bounded_float) noexcept -> bool = default; |
| 84 | + friend constexpr auto operator<=>(bounded_float, bounded_float) noexcept -> std::partial_ordering = default; |
| 85 | +
|
| 86 | + // Arithmetic (throw on IEEE 754 issues or out-of-range result) |
| 87 | + friend constexpr auto operator+(bounded_float, bounded_float) -> bounded_float; |
| 88 | + friend constexpr auto operator-(bounded_float, bounded_float) -> bounded_float; |
| 89 | + friend constexpr auto operator*(bounded_float, bounded_float) -> bounded_float; |
| 90 | + friend constexpr auto operator/(bounded_float, bounded_float) -> bounded_float; |
| 91 | + friend constexpr auto operator%(bounded_float, bounded_float) -> bounded_float; |
| 92 | +
|
| 93 | + // Compound assignment |
| 94 | + constexpr auto operator+=(bounded_float) -> bounded_float&; |
| 95 | + constexpr auto operator-=(bounded_float) -> bounded_float&; |
| 96 | + constexpr auto operator*=(bounded_float) -> bounded_float&; |
| 97 | + constexpr auto operator/=(bounded_float) -> bounded_float&; |
| 98 | + constexpr auto operator%=(bounded_float) -> bounded_float&; |
| 99 | +}; |
| 100 | +
|
| 101 | +} // namespace boost::safe_numbers |
| 102 | +---- |
| 103 | + |
| 104 | +`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. |
| 105 | + |
| 106 | +== Exception Behavior |
| 107 | + |
| 108 | +|=== |
| 109 | +| Condition | Exception Type |
| 110 | + |
| 111 | +| Value outside `[Min, Max]` at construction or after arithmetic | `std::domain_error` |
| 112 | +| NaN at construction | `std::domain_error` |
| 113 | +| Signaling NaN at construction | `std::domain_error` |
| 114 | +| Addition / subtraction overflow to +infinity | `std::overflow_error` |
| 115 | +| Addition / subtraction underflow to -infinity | `std::underflow_error` |
| 116 | +| Multiplication overflow | `std::overflow_error` |
| 117 | +| Multiplication underflow | `std::underflow_error` |
| 118 | +| Division producing NaN (e.g., 0/0, inf/inf) | `std::domain_error` |
| 119 | +| Division by zero (finite numerator) | `std::domain_error` |
| 120 | +| Modulo with zero divisor | `std::domain_error` |
| 121 | +| Modulo with infinite numerator | `std::domain_error` |
| 122 | +| Narrowing conversion (e.g., f64 -> f32) overflowing to infinity | `std::overflow_error` |
| 123 | +|=== |
| 124 | + |
| 125 | +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. |
| 126 | + |
| 127 | +== Mixed-Width Operations |
| 128 | + |
| 129 | +Operations between `bounded_float` types with different bounds are compile-time errors: |
| 130 | + |
| 131 | +[source,c++] |
| 132 | +---- |
| 133 | +bounded_float<-1.0f, 1.0f> a {f32{0.5f}}; |
| 134 | +bounded_float<-2.0f, 2.0f> b {f32{0.5f}}; |
| 135 | +
|
| 136 | +// auto c = a + b; // Compile error: different bounds |
| 137 | +---- |
| 138 | + |
| 139 | +== Notes on `static_cast` to the basis type |
| 140 | + |
| 141 | +`float_basis` (the implementation of `f32` / `f64`) declares an explicitly-deleted catch-all constructor that intercepts any `static_cast<f32>(other)` where `other` is not already a `float`. As a result, `static_cast<f32>(my_bounded_float)` will fail to compile with a "uses deleted function" error, even though a conversion operator exists. Use one of: |
| 142 | + |
| 143 | +[source,c++] |
| 144 | +---- |
| 145 | +auto raw {static_cast<float>(my_bounded_float)}; // converts to fundamental float |
| 146 | +auto basis {my_bounded_float.to_basis()}; // returns f32 directly |
| 147 | +auto built {f32{static_cast<float>(my_bounded_float)}}; // explicit f32 build |
| 148 | +---- |
| 149 | + |
| 150 | +== Standard Library Support |
| 151 | + |
| 152 | +`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<bounded_float<Min, Max>>` is also specialized in `<boost/safe_numbers/limits.hpp>`, with `is_iec559`, `has_infinity`, `has_quiet_NaN`, and `has_signaling_NaN` set to `false` to reflect that the type rejects those values. |
0 commit comments