<stdfloat>
C++23 header providing optional fixed-width extended floating-point types including IEEE 754 half, single, double, quad, and bfloat16 formats.
<stdfloat>since C++23A C++23 standard library header that introduces up to five optional, implementation-defined fixed-width floating-point types with well-specified IEEE 754 formats, each available only when the underlying hardware and compiler toolchain support it.
Overview
Before C++23, the language offered float, double, and long double, whose underlying representations were implementation-defined. On virtually every modern platform, float maps to IEEE 754 binary32 and double to binary64, but the standard made no such guarantee. <stdfloat> closes that gap by providing named aliases tied to specific IEEE 754 formats.
The five types, all in namespace std, are:
| Type | IEEE Format | Total Bits | Significand Bits | Exponent Bits | Approx. Decimal Digits |
|---|---|---|---|---|---|
std::float16_t | binary16 (half) | 16 | 11 | 5 | ~3.3 |
std::bfloat16_t | bfloat16 | 16 | 8 | 8 | ~2.7 |
std::float32_t | binary32 (single) | 32 | 24 | 8 | ~7.2 |
std::float64_t | binary64 (double) | 64 | 53 | 11 | ~15.9 |
std::float128_t | binary128 (quad) | 128 | 113 | 15 | ~34 |
All five types are optional. An implementation provides a type only if the platform natively supports that format. Whether each type is available is indicated by a corresponding predefined macro:
#if defined(__STDCPP_FLOAT16_T__)
// std::float16_t is available
#endif
#if defined(__STDCPP_BFLOAT16_T__)
// std::bfloat16_t is available
#endif
#if defined(__STDCPP_FLOAT32_T__)
// std::float32_t is available
#endif
#if defined(__STDCPP_FLOAT64_T__)
// std::float64_t is available
#endif
#if defined(__STDCPP_FLOAT128_T__)
// std::float128_t is available
#endifOn x86-64 with GCC or Clang targeting AVX-512 FP16, you will typically find float16_t, bfloat16_t, float32_t, and float64_t all present. float128_t availability varies widely: GCC supports it on most targets via __float128; MSVC does not.
When a type is available, std::float32_t may simply be an alias for float and std::float64_t for double if those types happen to match the required IEEE 754 format β which is almost universally true. The value of the <stdfloat> types is therefore strongest for float16_t, bfloat16_t, and float128_t, formats that have no traditional C++ counterpart.
Syntax
#include <stdfloat> // C++23
// Literal suffixes β each requires the corresponding type to exist
std::float16_t a = 1.0f16;
std::bfloat16_t b = 1.0bf16;
std::float32_t c = 1.0f32;
std::float64_t d = 1.0f64;
std::float128_t e = 1.0f128;The suffix casing is flexible β f16, F16, bf16, BF16, f32, F32, f64, F64, f128, F128 are all valid.
These types participate fully in standard library facilities:
#include <stdfloat>
#include <limits>
#include <numbers> // C++20
#if defined(__STDCPP_FLOAT16_T__)
constexpr auto pi_f16 = std::numbers::pi_v<std::float16_t>; // C++20 + C++23
constexpr auto max_f16 = std::numeric_limits<std::float16_t>::max();
constexpr auto eps_f16 = std::numeric_limits<std::float16_t>::epsilon();
#endifHardware and Compiler Support
Not all five types exist on every platform. The standard makes them optional precisely because hardware support varies significantly across architectures. The following table summarises which formats have native arithmetic support and which compilers expose them via <stdfloat>:
| Type | x86 (AVX-512 FP16) | x86 (AVX-512 BF16) | ARM (ARMv8.6-A+) | NVIDIA GPU (CUDA) | Google TPU |
|---|---|---|---|---|---|
float16_t | Native FP16 arithmetic (Sapphire Rapids+) | Software emulation | Software emulation | Native (sm_53+) | Software emulation |
bfloat16_t | Software emulation pre-Sapphire Rapids; native BF16 on Cooper Lake+ | Native BF16 (Cooper Lake+) | Native BF16 (ARMv8.6-A, Apple M2/A15+, AMD Zen 4) | Native (sm_80+, Ampere+) | Native |
float32_t | Always available (= float) | Always available | Always available | Always available | Available |
float64_t | Always available (= double) | Always available | Always available | Limited (sm_60+) | Limited |
float128_t | GCC extension __float128; no native instruction | No native instruction | No native instruction | Not supported | Not supported |
Compiler support status (as of C++23 ratification):
| Compiler | <stdfloat> header | float16_t / bfloat16_t | float32_t / float64_t | float128_t | Notes |
|---|---|---|---|---|---|
| GCC 13+ | Yes (P1467R9) | Yes (with -mf16c/-mavx512fp16 for hardware) | Yes (aliases for float/double) | Yes (via __float128) | Full C++23 conformant |
| Clang 17+ | Partial | Core language _Float16 support; std::float16_t requires libc++ 18+ | Yes | Platform-dependent | libc++ support lags behind language support |
| MSVC 19.44+ | Partial (<stdfloat> header exists) | Not yet (open request) | Yes | No | No float16_t/bfloat16_t as of VS 2022 17.x; header declares empty namespace only on unsupported types |
| Apple Clang | Follows upstream Clang | Platform-specific | Yes | No | ARMv8.6-A BF16 available on Apple Silicon with appropriate SDK flags |
float32_t and float64_t are defined on every conforming C++23 implementation where float is IEEE 754 binary32 and double is binary64 β which is virtually every modern platform. The exotic types (float16_t, bfloat16_t, float128_t) are where you need to guard with the availability macros.
#include <stdfloat>
// Compile-time capability probe pattern
#if defined(__STDCPP_FLOAT16_T__) && defined(__STDCPP_BFLOAT16_T__)
// Full ML half-precision path
#elif defined(__STDCPP_FLOAT32_T__) && defined(__STDCPP_FLOAT64_T__)
// Baseline path β available everywhere
#else
#error "Minimal <stdfloat> support required"
#endifConversion Rules
C++23 (via P1467R9) imposes stricter conversion rules for extended floating-point types than exist for the legacy float/double/long double types. The core rule is:
A conversion between two floating-point types, when at least one is an extended floating-point type, is implicit only if the destination type has a greater or equal conversion rank than the source type. Any conversion that is potentially lossy must be explicit.
This means widening is implicit, narrowing is not:
| Conversion | Direction | Implicit? | Notes |
|---|---|---|---|
float16_t β float32_t | Widening | Yes | Every float16_t value is exactly representable as float32_t |
float32_t β float64_t | Widening | Yes | Standard lossless promotion |
float64_t β float128_t | Widening | Yes (if float128_t available) | |
bfloat16_t β float32_t | Widening | Yes | Every bfloat16_t value is exactly representable as float32_t |
float32_t β float16_t | Narrowing | No β explicit cast required | Can overflow to Β±inf or lose precision |
float32_t β bfloat16_t | Narrowing | No β explicit cast required | Truncates 16 mantissa bits |
float16_t β bfloat16_t | Neither is a superset | Ill-formed without explicit cast | These two types have unordered conversion ranks |
The float16_t and bfloat16_t types are unordered in conversion rank β neither is a widening of the other because they have overlapping but non-subsetted value sets. Mixing them directly is a compile error; you must go through float32_t.
#include <stdfloat>
#if defined(__STDCPP_FLOAT16_T__) && defined(__STDCPP_BFLOAT16_T__) && defined(__STDCPP_FLOAT32_T__)
std::float16_t a = 1.5f16;
std::float32_t b = a; // OK: implicit widening
std::bfloat16_t c = static_cast<std::bfloat16_t>(b); // OK: explicit narrowing
// std::bfloat16_t d = a; // Error: unordered conversion rank, ill-formed
// Mixed-precision arithmetic promotes to the wider type:
// 1.5f16 + 1.0f32 β operands: float16_t widened to float32_t β result is float32_t
auto result = a + b; // result is std::float32_t
#endifNote that legacy conversions (double β float, long double β double) remain implicit to preserve compatibility with existing code. The stricter rules apply only when an extended floating-point type is involved on at least one side.
std::numeric_limits Quick-Reference
All available <stdfloat> types have full std::numeric_limits specializations. The values below are derived from their IEEE 754 format definitions and confirmed by the P1467R9 standard proposal and IEEE 754-2019:
| Member | float16_t (binary16) | bfloat16_t (bfloat16) | float32_t (binary32) | float64_t (binary64) |
|---|---|---|---|---|
digits (mantissa bits, including implicit leading 1) | 11 | 8 | 24 | 53 |
digits10 (reliable decimal digits) | 3 | 2 | 6 | 15 |
epsilon() (2^(1βdigits)) | ~9.766Γ10β»β΄ (2β»ΒΉβ°) | ~7.813Γ10β»Β³ (2β»β·) | ~1.192Γ10β»β· (2β»Β²Β³) | ~2.220Γ10β»ΒΉβΆ (2β»β΅Β²) |
max() | 65504 | ~3.390Γ10Β³βΈ | ~3.403Γ10Β³βΈ | ~1.798Γ10Β³β°βΈ |
min() (smallest positive normal) | ~6.104Γ10β»β΅ (2β»ΒΉβ΄) | ~1.175Γ10β»Β³βΈ (2β»ΒΉΒ²βΆ) | ~1.175Γ10β»Β³βΈ (2β»ΒΉΒ²βΆ) | ~2.225Γ10β»Β³β°βΈ (2β»ΒΉβ°Β²Β²) |
max_exponent | 16 | 128 | 128 | 1024 |
is_iec559 | true | false | true | true |
The critical point about is_iec559: bfloat16_t is not an IEEE 754 standard format β it was designed by Google Brain as a truncated float32_t. It supports NaN and infinity using the same encoding as float32_t, but it is not defined in IEEE 754-2019. Code that conditionally enables special behaviour based on is_iec559 will correctly see false for bfloat16_t.
#include <stdfloat>
#include <limits>
#include <cassert>
#if defined(__STDCPP_FLOAT16_T__) && defined(__STDCPP_BFLOAT16_T__)
static_assert(std::numeric_limits<std::float16_t>::digits == 11);
static_assert(std::numeric_limits<std::bfloat16_t>::digits == 8);
static_assert(std::numeric_limits<std::float16_t>::is_iec559 == true);
static_assert(std::numeric_limits<std::bfloat16_t>::is_iec559 == false);
// bfloat16 has much lower precision but same dynamic range as float32
static_assert(std::numeric_limits<std::bfloat16_t>::max_exponent ==
std::numeric_limits<std::float32_t>::max_exponent); // both 128
#endifExamples
ML inference buffer layout
The canonical use case for float16_t and bfloat16_t is memory-efficient storage of neural network weights and activations. bfloat16_t is preferred over float16_t when dynamic range matters more than precision, because it shares the same 8-bit exponent as float32_t:
#include <stdfloat>
#include <vector>
#include <span>
#include <cstring>
#if defined(__STDCPP_BFLOAT16_T__) && defined(__STDCPP_FLOAT32_T__)
// Quantize a float32 buffer to bfloat16 by truncating the low 16 significand bits.
void quantize_to_bf16(std::span<const std::float32_t> src,
std::span<std::bfloat16_t> dst)
{
for (std::size_t i = 0; i < src.size(); ++i) {
std::uint32_t bits;
std::memcpy(&bits, &src[i], 4);
bits >>= 16;
std::uint16_t bf;
std::memcpy(&bf, &bits, 2);
dst[i] = std::bit_cast<std::bfloat16_t>(bf); // C++20
}
}
#endifThis truncation trick works because bfloat16_t and float32_t share the same exponent layout; the value magnitude is preserved and only mantissa precision is lost.
High-precision accumulation
#include <stdfloat>
#include <numeric>
#include <vector>
#if defined(__STDCPP_FLOAT16_T__) && defined(__STDCPP_FLOAT64_T__)
// Accumulate float16 data in float64 to avoid catastrophic cancellation.
std::float64_t sum_f16(std::span<const std::float16_t> data)
{
return std::transform_reduce(
data.begin(), data.end(),
std::float64_t{0},
std::plus<>{},
[](std::float16_t v) { return static_cast<std::float64_t>(v); }
);
}
#endifSumming half-precision values directly accumulates rounding error at an alarming rate. Widening to float64_t for the accumulator is the standard pattern.
Portable quad-precision polynomial evaluation
#include <stdfloat>
#include <array>
#if defined(__STDCPP_FLOAT128_T__)
// Evaluate a degree-4 polynomial using compensated quad-precision arithmetic.
std::float128_t horner(std::float128_t x,
std::span<const std::float128_t> coeffs)
{
std::float128_t result{0};
for (auto it = coeffs.rbegin(); it != coeffs.rend(); ++it)
result = result * x + *it;
return result;
}
#endiffloat128_t is the right tool when you need to verify a double algorithm's numerical stability: implement a reference version in float128_t and compare outputs.
Best Practices
Guard every use with the availability macro. The types are optional by specification. Unconditional use will fail to compile on implementations that do not support the format. Wrap platform-specific paths in #if defined(__STDCPP_FLOAT16_T__) rather than relying on target architecture detection.
Use bfloat16_t for ML weight storage, float16_t for GPU rasterization. The two 16-bit formats have different strengths: bfloat16_t trades precision for dynamic range and is essentially lossless for the exponent field of float32_t; float16_t offers better mantissa precision within a narrower dynamic range, which suits color and geometry data that lives in [0, 1] or small bounded ranges.
Accumulate in wider types. Never reduce partial sums in float16_t or bfloat16_t. The error growth from repeated rounding dominates even simple dot products. Accumulate in float32_t or float64_t, then convert the result.
Prefer std::numbers::pi_v<T> over cast expressions. When you need mathematical constants at a specific precision, std::numbers::pi_v<std::float16_t> gives you the best representable approximation in that format, whereas static_cast<std::float16_t>(3.14159265358979) may introduce a double-rounding error.
Treat float32_t and float64_t as documentation, not a portability layer. On any platform where they exist, they are aliases for float and double respectively. Their value is semantic clarity β a function signature void process(std::float32_t*) communicates the format requirement explicitly.
Common Pitfalls
Assuming the types always exist. Code that compiles cleanly on an AVX-512-capable x86 server will fail on embedded targets, WASM, or older MSVC toolchains. The standard explicitly calls these types optional.
Mixing literal suffixes across translation units. If one TU uses 1.5f16 and another uses static_cast<std::float16_t>(1.5f), the results should be identical β but if you mix with a third-party library that defines its own half-precision type (e.g., __fp16 or Eigen::half), implicit conversions may silently reinterpret bit patterns. Always convert explicitly.
Expecting standard math functions to work. As of C++23, <cmath> overloads for these extended types are implementation-defined. std::sqrt(std::float16_t{4.0f16}) may not compile. Convert to float32_t or float64_t before passing to math functions, then convert back if necessary.
Narrowing conversions in aggregate initialization. The usual narrowing rules apply. std::float16_t x = 70000.0f16; is undefined (70000 exceeds float16_t's maximum of 65504), and the value will be +inf. Use std::numeric_limits<std::float16_t>::max() or static assertions when initializing from computed constants.
Numeric properties and std::numeric_limits
Every available <stdfloat> type has a complete std::numeric_limits specialization, just like built-in floating-point types. This lets you write type-generic numeric algorithms that query format properties at compile time without hard-coding magic constants.
// C++23
#include <stdfloat>
#include <limits>
template <typename F>
constexpr F safe_reciprocal(F x) {
constexpr F tiny = std::numeric_limits<F>::min(); // smallest positive normal
if (x > -tiny && x < tiny)
return std::numeric_limits<F>::infinity();
return F{1} / x;
}
#if defined(__STDCPP_FLOAT16_T__) && defined(__STDCPP_FLOAT32_T__)
static_assert(std::numeric_limits<std::float16_t>::digits == 11);
static_assert(std::numeric_limits<std::float32_t>::digits == 24);
// float16_t epsilon β 2^-10, float32_t epsilon β 2^-23
static_assert(std::numeric_limits<std::float16_t>::epsilon() >
std::numeric_limits<std::float32_t>::epsilon());
#endifThe digits10 member tells you how many decimal digits roundtrip faithfully, which is the practical precision bound: 3 for float16_t, 2 for bfloat16_t, 6 for float32_t, and 15 for float64_t. Use this instead of hard-coded digit counts in formatting or comparison routines.
// C++23
#include <stdfloat>
#include <limits>
#include <format> // C++20
#if defined(__STDCPP_FLOAT16_T__)
// Print a float16 value with exactly as many digits as are meaningful.
std::string to_decimal(std::float16_t v) {
int prec = std::numeric_limits<std::float16_t>::digits10;
return std::format("{:.{}g}", static_cast<float>(v), prec);
}
#endifArithmetic and implicit conversions
Arithmetic between <stdfloat> types follows a promotion hierarchy: operands are implicitly widened to the type with the highest conversion rank before the operation is performed. Narrowing β going to a lower-rank type β always requires an explicit cast, preventing silent precision loss.
// C++23
#include <stdfloat>
#if defined(__STDCPP_FLOAT16_T__) && defined(__STDCPP_FLOAT32_T__) && defined(__STDCPP_FLOAT64_T__)
std::float16_t a = 1.5f16;
std::float32_t b = 2.0f32;
std::float64_t c = 3.0f64;
auto r1 = a + b; // float16_t promoted to float32_t; result is float32_t
auto r2 = b + c; // float32_t promoted to float64_t; result is float64_t
auto r3 = a + c; // float16_t promoted to float64_t; result is float64_t
// Explicit narrowing is required:
std::float16_t narrow = static_cast<std::float16_t>(r1);
// Mixing with built-in float/double follows the usual arithmetic conversion rules;
// float32_t and float64_t are aliases in practice, so no surprises there.
double d = r2; // float64_t β double: implicit (they are the same type on conformant platforms)
#endifThe bfloat16_t and float16_t types have unordered conversion ranks. Neither is a subset of the other's value set, so direct arithmetic between them is ill-formed without an intermediate explicit cast through float32_t.
// C++23
#include <stdfloat>
#if defined(__STDCPP_FLOAT16_T__) && defined(__STDCPP_BFLOAT16_T__) && defined(__STDCPP_FLOAT32_T__)
std::float16_t h = 1.0f16;
std::bfloat16_t bh = 2.0bf16;
// auto bad = h + bh; // Compile error: unordered conversion ranks
// Correct idiom: upcast both to float32_t first
auto good = static_cast<std::float32_t>(h) + static_cast<std::float32_t>(bh);
#endifstd::float16_t in ML and GPU workloads
std::float16_t (IEEE binary16) stores weights and activations at half the memory cost of float32_t, roughly doubling effective cache and memory bandwidth on supported hardware. Modern GPU and CPU tensor cores operate natively in FP16, making it the default numeric type for deep-learning inference on NVIDIA (sm_53+), AMD (RDNA 2+), and Intel (Sapphire Rapids AVX-512 FP16) hardware.
// C++23
#include <stdfloat>
#include <vector>
#include <span>
#include <numeric>
#if defined(__STDCPP_FLOAT16_T__) && defined(__STDCPP_FLOAT32_T__)
// Dot product: accumulate in float32 to prevent rounding blow-up.
std::float32_t dot_f16(std::span<const std::float16_t> a,
std::span<const std::float16_t> b)
{
std::float32_t acc{0};
for (std::size_t i = 0; i < a.size(); ++i)
acc += static_cast<std::float32_t>(a[i]) * static_cast<std::float32_t>(b[i]);
return acc;
}
// Convert a float32 model checkpoint to float16 for inference.
std::vector<std::float16_t>
to_fp16(std::span<const std::float32_t> weights)
{
std::vector<std::float16_t> out(weights.size());
for (std::size_t i = 0; i < weights.size(); ++i)
out[i] = static_cast<std::float16_t>(weights[i]);
return out;
}
#endifWatch for overflow: float16_t saturates at Β±65504. Activations with large magnitudes β common in the early layers of unquantized models β must be clipped or the values become Β±inf silently.
// C++23
#include <stdfloat>
#include <algorithm>
#include <limits>
#if defined(__STDCPP_FLOAT16_T__) && defined(__STDCPP_FLOAT32_T__)
std::float16_t safe_cast_f16(std::float32_t v) {
constexpr std::float32_t kMax = static_cast<std::float32_t>(
std::numeric_limits<std::float16_t>::max());
v = std::clamp(v, -kMax, kMax);
return static_cast<std::float16_t>(v);
}
#endifstd::bfloat16_t: the ML standard
std::bfloat16_t (Brain Float 16) is the industry-dominant training format precisely because it shares the 8-bit exponent of float32_t, giving it the same dynamic range (~10Β³βΈ). Converting between bfloat16_t and float32_t is a simple 16-bit shift with no exponent adjustment, making quantization and dequantization cheap.
// C++23
#include <stdfloat>
#include <bit> // C++20
#include <cstdint>
#include <cstring>
#if defined(__STDCPP_BFLOAT16_T__) && defined(__STDCPP_FLOAT32_T__)
// Lossless round-trip: float32 β bfloat16 β float32 preserves the upper 16 bits.
// Precision is reduced (7 mantissa bits instead of 23) but exponent is exact.
std::float32_t bf16_roundtrip(std::float32_t v) {
// Truncate lower 16 bits of the mantissa via bfloat16 round-trip.
auto bf = static_cast<std::bfloat16_t>(v);
return static_cast<std::float32_t>(bf);
}
// Stochastic rounding: add random noise before truncation to reduce bias.
std::bfloat16_t stochastic_round(std::float32_t v, std::uint16_t rand_bits) {
std::uint32_t bits;
std::memcpy(&bits, &v, 4);
bits += rand_bits; // add random noise to the lower 16 bits
std::float32_t noisy;
std::memcpy(&noisy, &bits, 4);
return static_cast<std::bfloat16_t>(noisy);
}
#endifbfloat16_t is not an IEEE 754 format (std::numeric_limits<std::bfloat16_t>::is_iec559 == false), but it does support NaN and infinity with the same bit patterns as float32_t. Code that checks is_iec559 as a precondition for safe arithmetic must handle bfloat16_t separately.
Compiler support and hardware requirements
The availability macros are the only reliable portability mechanism β do not infer type support from architecture macros like __AVX512FP16__ or __ARM_FEATURE_BF16, because the compiler's <stdfloat> support is a separate question from whether a given ISA extension is enabled.
// C++23
#include <stdfloat>
// Capability matrix β encode at compile time which paths are available.
struct FloatCaps {
static constexpr bool has_f16 = defined(__STDCPP_FLOAT16_T__);
static constexpr bool has_bf16 = defined(__STDCPP_BFLOAT16_T__);
static constexpr bool has_f32 = defined(__STDCPP_FLOAT32_T__);
static constexpr bool has_f64 = defined(__STDCPP_FLOAT64_T__);
static constexpr bool has_f128 = defined(__STDCPP_FLOAT128_T__);
};The table below summarises what to expect across mainstream toolchains as of C++23:
| Toolchain | float16_t | bfloat16_t | float128_t |
|---|---|---|---|
| GCC 13+ (x86-64) | Yes (-mavx512fp16 for HW) | Yes | Yes (soft via __float128) |
| GCC 13+ (AArch64) | Yes | Yes (ARMv8.6+) | No |
| Clang 17 + libc++ 18 (x86-64) | Yes | Yes | Platform-dependent |
| MSVC 19.44 (x86-64) | No (pending) | No (pending) | No |
| Apple Clang (Apple Silicon) | Yes | Yes (M2/A15+) | No |
| NVCC (CUDA 12+) | Via cuda_fp16.h bridge | Via cuda_bf16.h bridge | No |
GCC is the most complete implementation today. If you need portable float16_t on MSVC, fall back to a third-party half-precision library and expose a conversion bridge behind the availability macro.
Mixing float types safely
Mixing <stdfloat> types with legacy float/double values is usually safe, but the direction matters. On conformant platforms std::float32_t is float and std::float64_t is double, so those conversions are trivially implicit. The danger is mixing float16_t or bfloat16_t with legacy types through a chain of implicit promotions that inadvertently widens to double before narrowing β carrying a rounding step you did not intend.
// C++23
#include <stdfloat>
#if defined(__STDCPP_FLOAT16_T__) && defined(__STDCPP_FLOAT32_T__)
void safe_mix_example() {
std::float16_t h = 1.5f16;
// Safe: explicit widening path
float f = static_cast<std::float32_t>(h); // float32_t β‘ float
double d = static_cast<std::float64_t>(h); // float64_t β‘ double
// Dangerous pattern: implicit promotion through legacy arithmetic
// h + 1.0f β h widened to float, then added β fine here
// h + 1.0 β h widened to double β might surprise you in templates
auto via_double = h + 1.0; // result type: double, NOT float16_t
// In templates, pin the type explicitly:
auto result = h + static_cast<std::float16_t>(1.0f16); // stays float16_t
(void)f; (void)d; (void)via_double; (void)result;
}
#endifWhen writing type-generic code, constrain the template to a single floating-point type and let the caller decide the precision. Accepting multiple types and mixing them internally defeats the type safety that <stdfloat> provides.
// C++23
#include <stdfloat>
#include <concepts>
// Good: caller fixes the precision.
template <std::floating_point F>
F lerp(F a, F b, F t) {
return a + t * (b - a);
}
#if defined(__STDCPP_FLOAT16_T__)
// Usage:
// auto r = lerp(0.0f16, 1.0f16, 0.5f16); // stays in float16_t throughout
#endifstd::float128_t for high-precision computing
std::float128_t provides ~34 decimal digits of precision β enough to serve as a reference oracle when validating double algorithms or when intermediate computation must accumulate without catastrophic cancellation. It is most commonly used in scientific computing, arbitrary-precision interval arithmetic, and numerical stability analysis.
// C++23
#include <stdfloat>
#if defined(__STDCPP_FLOAT128_T__)
// Kahan compensated summation in quad precision.
std::float128_t kahan_sum(std::span<const std::float64_t> data)
{
std::float128_t sum{0}, comp{0};
for (auto v : data) {
std::float128_t y = static_cast<std::float128_t>(v) - comp;
std::float128_t t = sum + y;
comp = (t - sum) - y;
sum = t;
}
return sum;
}
// Validate a double result by comparing against a quad-precision reference.
bool result_is_accurate(double computed, std::span<const double> inputs,
double tolerance = 1e-12)
{
std::float128_t ref = kahan_sum(inputs);
std::float128_t err = static_cast<std::float128_t>(computed) - ref;
if (err < std::float128_t{0}) err = -err;
return static_cast<double>(err) < tolerance;
}
#endifPlatform caveats for float128_t are significant. On x86-64, GCC implements it via a software library (libquadmath); there is no 128-bit FP instruction. This makes float128_t arithmetic 10β100Γ slower than double. MSVC has no support at all. For production numerics, prefer double with compensated summation; reserve float128_t for offline validation and test oracles.
// C++23
#include <stdfloat>
#include <limits>
#if defined(__STDCPP_FLOAT128_T__)
// float128_t numeric properties
static_assert(std::numeric_limits<std::float128_t>::digits == 113);
static_assert(std::numeric_limits<std::float128_t>::digits10 == 33);
static_assert(std::numeric_limits<std::float128_t>::is_iec559 == true);
#endifSee Also
std::numeric_limitsβ specializations exist for all available<stdfloat>typesstd::numbers::pi_v<T>β variable template for mathematical constants at any floating-point precision (C++20)<charconv>βstd::to_chars/std::from_charssupport for standard floating-point types; extended type support is implementation-defined in C++23