fix: fixed-point arithmetic for integer unit conversions (#580)#764
Merged
fix: fixed-point arithmetic for integer unit conversions (#580)#764
Conversation
Introduce a fixed-point implementation for unit conversions involving
integer representations, avoiding loss of significant digits that
previously occurred when the conversion factor was not a whole number.
New files:
- src/core/include/mp-units/bits/fixed_point.h: double_width_int<T> and
fixed_point<T,n> types for exact rational scaling of integer values.
Uses __int128 when available (__SIZEOF_INT128__) for 64-bit integers.
- src/core/include/mp-units/framework/scaling.h: public scaling_traits<>
customization point and scale<To>(M, value) free function. Provides
built-in specializations for floating-point and integer-like types.
- test/static/fixed_point_test.cpp: static assertions for the new types.
- test/runtime/fixed_point_test.cpp: runtime arithmetic edge-case tests.
Modified:
- sudo_cast.h: replace hand-rolled conversion_value_traits / sudo_cast_value
machinery with a single scale<To::rep>(c_mag, ...) call.
- representation_concepts.h: add MagnitudeScalable concept; replace
ComplexScalar with HasComplexOperations (which is its definition).
- customization_points.h: add unspecified_rep tag and declare the primary
scaling_traits<> template.
- framework.h / CMakeLists.txt: wire in the new headers.
- hacks.h: add MP_UNITS_DIAGNOSTIC_IGNORE_PEDANTIC and
MP_UNITS_DIAGNOSTIC_IGNORE_SIGN_CONVERSION macros.
- example/measurement.cpp: add scaling_traits specializations for
measurement<T> to demonstrate the customization point.
- test/static/{international,usc}_test.cpp: disable two tests that are
blocked on issue #614.
Co-authored-by: Tobias Hanhart <burnpanck@users.noreply.github.com>
The partial specialization for types with a nested value_type used 'value_Type' (capital T) instead of 'value_type', making the entire specialization dead code as the requires-clause could never be satisfied. Also fix 'mantiassa' -> 'mantissa' in the adjacent comment.
- 'quantitiy' -> 'quantity' - 'dictatet' -> 'dictated' - 'convetrible' -> 'convertible' - 'implemenation' -> 'implementation' - 'availabe' -> 'available'
When resolving the merge conflict in representation_concepts.h, the
PR's renamed version of the concept ('HasComplexOperations') was used
instead of master's established name ('ComplexScalar'). The two concepts
are semantically equivalent — burnpanck simply renamed it in his branch.
Revert to the canonical 'ComplexScalar' name while retaining the new
'MagnitudeScalable' concept which was the actual addition from the PR.
The PR branched from a version where measurement<T> was defined inline in measurement.cpp. Master later moved the class to example/include/ measurement.h and changed measurement.cpp to #include that header. The squash merge therefore introduced a duplicate definition: the class from the header and the PR's inline class were both visible, causing an 'ambiguous reference' error. Remove the now-redundant inline class; the scaling_traits specializations added by the PR work correctly with the class from measurement.h.
…on all platforms
On ARM / Apple Silicon, long double == double (64-bit mantissa). The old
fixed_point<T>(long double) initialiser lost ~12 bits of precision for 64-bit
integer types when representing the scaling ratio, producing an error of ~49
units for the 10/9 (degree → gradian) conversion with a 10^18 input value.
Fix by splitting the integer-path else-branch into two cases:
• Pure rational M (is_integral(M * (denominator(M) / numerator(M))) == true):
use (value * numerator) / denominator via double_width_int_for_t<> arithmetic.
This is exact on every platform regardless of long double width.
• Irrational M (involves π etc.): keep the long double fixed_point approximation.
These conversions are inherently approximate; small values still produce correct
truncated results on all platforms.
Update the test comment to reflect the new exact-arithmetic path.
Fixes CI failures on clang-18/ARM and apple-clang-16.
72.27 is not exactly representable as double (it rounds to 72.2699...96). Multiplying by the conversion factor 100/7227 via long double gives a result ≥ 1.0 on x86 (80-bit long double, 64-bit mantissa) only by chance, but 0.99999...978 on ARM / Apple Silicon where long double == double (52-bit). The correct mathematical statement is: 7227 tex_point = 100 inch (exact rational relationship). Use that integer form instead of the inexact 72.27 double literal so the test is correct and platform-independent.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Core change
Integer-to-integer unit conversions now use a double-width fixed-point representation (
detail::fixed_point<T>) for rational scaling factors that are neither integral nor have an integral inverse. This guarantees exact results without promoting tointmax_tor silently overflowing.Additional changes (made during integration)
Scaling API refactor:
scale<M>()free function customization point withscaling_traits<From, To>class template specialization — cleaner and more explicit.silent_cast<To>(value)for intentional narrowing casts with float-conversion diagnostics suppressed.is_integral_scaling(from, to)constevalhelper so users can writeimplicitly_scalablespecializations without touchingget_canonical_unitinternals.scaling_kind/get_scaling_kind(over-engineered, only used for one boolean check).scaling_overflows_non_zero_valuesback todetail::.RepConvertible→RepConvertibleFrom,RepAssignable→RepAssignableFrom(align with standard preposition convention).Clang 16 ICE fix:
template<UnitMagnitude auto M>insidescaling_traits_impl::scaleand user specializations changed totemplate<auto M>to avoid a clang-16 SIGSEGV in nested concept constraint checking.Documentation:
using_custom_representation_types.md: updated to describescaling_traits<From, To>specialization pattern.value_conversions.md: updated cross-reference.docs/examples/measurement.md: correct line references and added description of thescaling_traitsspecialization and theauto Mconstraint rationale.