1. Revision history
-
R0: Initial revision.
2. Motivation
2.1. SIMD-generic numeric code
The SIMD working draft introduces as an element-wise parallel extension of an element type . A central goal is to allow the same generic numeric code to operate on scalar and on interchangeably. Most non-trivial numeric code that aims for that behaviour would require somewhere. For example:
template < class V > bool nearly_equal ( V a , V b ) { return std :: abs ( a - b ) <= std :: numeric_limits < V >:: epsilon () * std :: max ( std :: abs ( a ), std :: abs ( b )); }
Today, instantiating that function with a simd type is ill-formed, because no specialization of is provided for . The user must either rewrite the algorithm to refer to , or abandon the idea of simd-generic behaviour for this function.
The same gap appears in concept-constrained code that tests members such as , , or : silently fails such constraints even though it morally should satisfy them. Also, in standard library facilities specified in terms of , such as , , , or the distributions, they are blocked from any future SIMD-generic generalisation while the trait has no answer for .
already provides element-wise arithmetic, comparison, and the standard math functions. It already presents itself as a numeric type. is the missing piece. Without it, is missing some meaningful behaviour, where numeric operations and functions work, but the standard mechanism for querying the range, precision, and representability of this type does not.
2.2. Why users should not broadcast manually
A reviewer might suppose that users should be required to write themselves, and leave unspecialized, but there are three reasons to do something better:
-
The whole point of
being a numeric type is that generic numeric code written against an arbitrarybasic_vec < T , Abi > should work. Forcing every author of generic code to explicitly handle SIMD types defeats this.V -
is the standard library’s mechanism for querying the type model ofnumeric_limits .V has a well-defined type model where it is the same asbasic_vec < T , Abi > , applied element-wise, so this relationship is preserved.T -
The same specialization, written once, automatically extends to future extended types (
,std :: float16_t ,std :: bfloat16_t , ...), and to any user-defined element type thatstd :: float128_t may admit, provided that element type specializesbasic_vec .numeric_limits
3. Design
3.1. numeric_limits as a description of the type model
3.2. numeric_limits as a description of the type model
describes the type model of (its range, precision, and representability), which is the type model of applied element-wise. It is not a runtime contract, just as scalar is not. For example, and already report values that need not match runtime behaviour under , FTZ/DAZ, or trap-disabling configurations, and there is no obligation for applied to to behave differently.
3.3. Element-wise application
Every member of falls into one of two categories:
-
Trait members — static data members of integral, boolean, or enumeration type that describe properties of the type (
,digits ,is_signed ,radix ,is_iec559 , ...).round_style -
Value-returning members — static member functions that return a value of
(T ,min () ,max () ,lowest () ,epsilon () ,infinity () , ...).quiet_NaN ()
For :
-
Trait members have the same value as the corresponding member of
. They describe the per-element type model.numeric_limits < T > -
Value-returning members return a
whose elements are each equal to the corresponding scalar limit, obtained by broadcasting throughbasic_vec < T , Abi > ’s broadcast constructor.basic_vec < T , Abi >
The user-selected ABI is preserved, so querying for returns values of exactly .
cv-qualified forms ( etc.) are handled by the existing partial specializations in per [numeric.limits.general], which inherit from the unqualified specialization. No additional specialization for cv-qualified is required.
3.4. When the specialization is provided
The specialization is provided only when is itself specialized (i.e., is true). For any other , the primary template applies and reports , matching the standard’s behaviour for any unsupported type.
This matters because is expected to allow user-defined element types in future [P2964R3]. Such types automatically gain a sensible as soon as they specialize , with no further library or standardization work.
3.5. Header and module placement
The specialization is provided by , where itself is declared. Including alone is not sufficient to make the specialization available, so users must include .
3.6. constexpr
provides a broadcast constructor. The value-returning members of the specialization are therefore whenever the corresponding members of are.
4. Design exploration
The following design choices were considered.
4.1. A separate trait class (e.g., std :: simd :: numeric_limits )
A parallel trait class specifically for SIMD types could be defined alongside . This was rejected because generic numeric code is written against , not against a SIMD-specific facility. A parallel trait does not compose with existing generic code, and creates an obligation to keep the two traits synchronised as evolves.
4.2. Member-by-member specification in the wording
The proposed wording could enumerate each member of individually, specifying its value or returned expression. This was rejected because every future change to (additions, deprecations, removals) would require a follow-up paper to keep the SIMD specialization in step. The element-wise rule chosen instead tracks automatically.
4.3. Multiplying trait values by the SIMD width
A reader might ask whether members such as , , or should reflect the aggregate properties of a rather than its element type. For example, by reporting . This is wrong. A of four s has 4 independent values with 24-bit precision, not one value with 96-bit precision. Generic code that uses to size a buffer or compute a tolerance would produce garbage. The element-wise interpretation is the only meaningful one.
4.4. Behavioural trait members
The members , , , , , , , and all carry an implicit suggestion of runtime behaviour. On real SIMD hardware:
-
FP exceptions are typically masked, so a SIMD operation does not trap on overflow even when scalar
would suggest it might.traps -
Some instructions ignore the dynamic rounding mode.
-
Denormal flushing (FTZ/DAZ) is common, suppressing the representations that
would suggest exist.has_denorm
Forwarding these members from is correct for the same reason it is correct for scalar . is the type model, not the runtime contract. Scalar reports the same values regardless of or the FTZ/DAZ state of the FP environment, and code that needs to know the runtime contract uses or compiler-specific facilities, not .
4.5. basic_mask is out of scope
The SIMD working draft also provides . Its elements are boolean, and its story would work with in exactly the same way. However, in our current experience SIMD-generic numeric code is never parameterized over a boolean element type. The trait members of interest in simd-generic code (, , , , etc.) are of no interest. A specialization for would therefore be of marginal utility, and is not proposed here.
4.6. Deprecated trait members
is deprecated in C++23 and is deprecated in C++26. Because the proposal specifies the specialization element-wise rather than member-by-member, removal of these members from propagates to without further specification. Implementations that enumerate members in code must of course track such removals, as they do elsewhere in the library.
5. Implementation experience
The proposal has been prototyped as a single-header partial specialization of against an existing implementation of . The entire specialization is approximately thirty lines of code (see Appendix - Reference implementation) and required no changes to itself. The existing broadcast constructor is sufficient. The prototype compiles with C++20 and more recent, and behaves as described above for the standard arithmetic element types.
6. Proposed wording
Wording is relative to the current C++ working draft and the SIMD working draft (cited here as [simd]).
Add a new subclause to [simd] called [simd.limits] which would have something like the following wording:
** Numeric limits [simd.limits]**
The header
provides a partial specialization of< simd > forstd :: numeric_limits . The specialization is provided if and only ifstd :: simd :: basic_vec < T , Abi > isstd :: numeric_limits < T >:: is_specialized true.Let
denoteS andstd :: numeric_limits < T > denoteV .std :: simd :: basic_vec < T , Abi >
For each static data member
ofm ,S has the same type and value asstd :: numeric_limits < V >:: m . In particular,S :: m equalsstd :: numeric_limits < V >:: is_specialized true.For each static member function
off whose return type isS ,T provides a static member functionstd :: numeric_limits < V > whose return type isf and whose returned value isV . The returned value is aV ( S :: f ()) expression ifconstexpr is.S :: f () [Note: The specialization describes the type model of
, which is the type model ofV applied element-wise. It does not describe the runtime behaviour of operations onT . — end note]V [Note: The cv-qualified forms
,numeric_limits < const V > , andnumeric_limits < volatile V > are provided by the existing partial specializations in [numeric.limits.general], which inherit from the unqualified specialization above. — end note]numeric_limits < const volatile V >
Appendix - Reference implementation
The following self-contained header provides the proposed specialization. It assumes is already available and provides a broadcast constructor. The cv-qualified forms are handled automatically by the existing partial specializations in .
#include <limits>#include <simd>namespace std { template < class T , class Abi > requires std :: numeric_limits < T >:: is_specialized struct numeric_limits < std :: simd :: basic_vec < T , Abi >> { private : using V = std :: simd :: basic_vec < T , Abi > ; using S = numeric_limits < T > ; public : static constexpr bool is_specialized = true; // Value-returning members: broadcast the scalar limit. static constexpr V min () noexcept { return V ( S :: min ()); } static constexpr V max () noexcept { return V ( S :: max ()); } static constexpr V lowest () noexcept { return V ( S :: lowest ()); } static constexpr V epsilon () noexcept { return V ( S :: epsilon ()); } static constexpr V round_error () noexcept { return V ( S :: round_error ()); } static constexpr V infinity () noexcept { return V ( S :: infinity ()); } static constexpr V quiet_NaN () noexcept { return V ( S :: quiet_NaN ()); } static constexpr V signaling_NaN () noexcept { return V ( S :: signaling_NaN ()); } static constexpr V denorm_min () noexcept { return V ( S :: denorm_min ()); } // Trait members: forward unchanged from numeric_limits<T>. static constexpr int digits = S :: digits ; static constexpr int digits10 = S :: digits10 ; static constexpr int max_digits10 = S :: max_digits10 ; static constexpr bool is_signed = S :: is_signed ; static constexpr bool is_integer = S :: is_integer ; static constexpr bool is_exact = S :: is_exact ; static constexpr int radix = S :: radix ; static constexpr int min_exponent = S :: min_exponent ; static constexpr int min_exponent10 = S :: min_exponent10 ; static constexpr int max_exponent = S :: max_exponent ; static constexpr int max_exponent10 = S :: max_exponent10 ; static constexpr bool has_infinity = S :: has_infinity ; static constexpr bool has_quiet_NaN = S :: has_quiet_NaN ; static constexpr bool has_signaling_NaN = S :: has_signaling_NaN ; static constexpr bool is_iec559 = S :: is_iec559 ; static constexpr bool is_bounded = S :: is_bounded ; static constexpr bool is_modulo = S :: is_modulo ; static constexpr bool traps = S :: traps ; static constexpr bool tinyness_before = S :: tinyness_before ; static constexpr float_round_style round_style = S :: round_style ; }; } // namespace std