Skip to content

Commit 495c76c

Browse files
authored
Merge pull request #204 from cppalliance/192
Add a bounded floating point type class
2 parents 4fb815c + 5bdd554 commit 495c76c

19 files changed

Lines changed: 1624 additions & 0 deletions

doc/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
* xref:signed_integers.adoc[]
3232
* xref:bounded_uint.adoc[]
3333
* xref:bounded_int.adoc[]
34+
* xref:bounded_float.adoc[]
3435
* xref:cuda.adoc[]
3536
* xref:literals.adoc[]
3637
* xref:limits.adoc[]
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
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

Comments
 (0)