From c51baae074276997a81fca9612f0a6aa8fa4d2ef Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Fri, 10 May 2024 23:13:44 +0200 Subject: [PATCH 1/7] value_cast overloads for ToQ and ToQP --- .../include/mp-units/framework/value_cast.h | 94 ++++++++++++++++++- test/static/quantity_point_test.cpp | 21 +++++ 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/src/core/include/mp-units/framework/value_cast.h b/src/core/include/mp-units/framework/value_cast.h index 1f92bceaa..d3fb7445f 100644 --- a/src/core/include/mp-units/framework/value_cast.h +++ b/src/core/include/mp-units/framework/value_cast.h @@ -80,7 +80,8 @@ template * * auto q = value_cast(1.23 * ms); * - * @tparam ToRep a representation type to use for a target quantity + * @tparam ToU a unit to use for the target quantity + * @tparam ToRep a representation type to use for the target quantity */ template requires Quantity> && (convertible(std::remove_reference_t::reference, ToU)) && @@ -92,6 +93,30 @@ template return detail::sudo_cast>(std::forward(q)); } + +/** + * @brief Explicit cast of a quantity's representation + * + * Implicit conversions between quantities of different types are allowed only for "safe" + * (e.g. non-truncating) conversion. In truncating cases an explicit cast have to be used. + * + * using ToQ = quantity; + * auto q = value_cast(1.23 * ms); + * + * Note that value_cast only changes the "representation aspects" (unit and representation + * type), but not the "meaning" (quantity type). + * + * @tparam ToQ a target quantity type to which to cast the representation + */ +template + requires Quantity> && (convertible(std::remove_reference_t::reference, ToQ::unit)) && + (ToQ::quantity_spec == std::remove_reference_t::quantity_spec) && + std::constructible_from::rep> +[[nodiscard]] constexpr Quantity auto value_cast(Q&& q) +{ + return detail::sudo_cast(std::forward(q)); +} + /** * @brief Explicit cast of a quantity point's unit * @@ -133,14 +158,15 @@ value_cast(QP&& qp) } /** - * @brief Explicit cast of a quantity's unit and representation type + * @brief Explicit cast of a quantity point's unit and representation type * * Implicit conversions between quantities of different types are allowed only for "safe" * (e.g. non-truncating) conversion. In truncating cases an explicit cast have to be used. * - * auto q = value_cast(1.23 * ms); + * auto qp = value_cast(quantity_point{1.23 * ms}); * - * @tparam ToRep a representation type to use for a target quantity + * @tparam ToU a unit to use for the target quantity + * @tparam ToRep a representation type to use for the target quantity */ template requires QuantityPoint> && (convertible(std::remove_reference_t::reference, ToU)) && @@ -152,4 +178,64 @@ template std::remove_reference_t::point_origin}; } +/** + * @brief Explicit cast of a quantity point's representation + * + * Implicit conversions between quantities of different types are allowed only for "safe" + * (e.g. non-truncating) conversion. In truncating cases an explicit cast have to be used. + * + * inline constexpr struct A : absolute_point_origin A; + * inline constexpr struct B : relative_point_origin B; + * + * using ToQP = quantity_point; + * auto qp = value_cast(quantity_point{1.23 * m}); + * + * Note that value_cast only changes the "representation aspects" (unit and representation + * type), but not the "meaning" (quantity type or the actual point that is being described). + * + * @tparam ToQ a target quantity type to which to cast the representation of the point + */ +template + requires QuantityPoint> && (convertible(std::remove_reference_t::reference, ToQ::unit)) && + (ToQ::quantity_spec == std::remove_reference_t::quantity_spec) && + std::constructible_from::rep> +[[nodiscard]] constexpr QuantityPoint auto value_cast(QP&& qp) +{ + return quantity_point{value_cast(std::forward(qp).quantity_from_origin_is_an_implementation_detail_), + std::remove_reference_t::point_origin}; +} + +/** + * @brief Explicit cast of a quantity point's representation, including potentially the point origin + * + * Implicit conversions between quantities of different types are allowed only for "safe" + * (e.g. non-truncating) conversion. In truncating cases an explicit cast have to be used. + * + * inline constexpr struct A : absolute_point_origin A; + * inline constexpr struct B : relative_point_origin B; + * + * using ToQP = quantity_point; + * auto qp = value_cast(quantity_point{1.23 * m}); + * + * Note that value_cast only changes the "representation aspects" (unit, representation + * type and point origin), but not the "meaning" (quantity type or the actual point that is + * being described). + * + * @tparam ToQP a target quantity point type to which to cast the representation of the point + */ +template + requires QuantityPoint> && + (convertible(std::remove_reference_t::reference, ToQP::unit)) && + (ToQP::quantity_spec == std::remove_reference_t::quantity_spec) && + (detail::same_absolute_point_origins(ToQP::point_origin, std::remove_reference_t::point_origin)) && + std::constructible_from::rep> +[[nodiscard]] constexpr QuantityPoint auto value_cast(QP&& qp) +{ + return quantity_point{ + value_cast(std::forward(qp).quantity_from_origin_is_an_implementation_detail_), + std::remove_reference_t::point_origin} + .point_for(ToQP::point_origin); +} + + } // namespace mp_units diff --git a/test/static/quantity_point_test.cpp b/test/static/quantity_point_test.cpp index acc53416e..d03d35758 100644 --- a/test/static/quantity_point_test.cpp +++ b/test/static/quantity_point_test.cpp @@ -1692,4 +1692,25 @@ static_assert(value_cast(lvalue_qp).quantity_from_zero().numerical_value_ static_assert(value_cast(lvalue_qp).quantity_from_zero().numerical_value_in(m) == 2000.f); } // namespace lvalue_tests +static_assert(value_cast>(quantity_point{2000 * m}).quantity_from_zero().numerical_value_in(km) == 2); +static_assert(value_cast>(quantity_point{2000 * m}).quantity_from_zero().numerical_value_in(km) == + 2); +static_assert( + !requires(quantity_point qp) { value_cast>(qp); }, + "value_cast shall not cast between different quantity types"); +static_assert( + !requires(quantity_point qp) { value_cast>(qp); }, + "value_cast shall not cast between different quantity types"); +static_assert(value_cast>(quantity_point{2 * km}) + .quantity_ref_from(mean_sea_level) + .numerical_value_in(m) == 2042); +static_assert(value_cast>(quantity_point{ + std::int8_t{100} * mm}) + .quantity_ref_from(mean_sea_level) + .numerical_value_in(cm) == 4210); +static_assert(value_cast>(quantity_point{4210 * cm}) + .quantity_ref_from(ground_level) + .numerical_value_in(mm) == 100); + + } // namespace From c0efdb1790e16d641b85edfc7dfc17d58475cf92 Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Sun, 12 May 2024 10:57:33 +0200 Subject: [PATCH 2/7] better handle simultaneous change of representation, unit and point_origin in value_cast, to prevent overflow in more cases. --- .../include/mp-units/framework/value_cast.h | 49 +++++++++++++++++-- test/static/quantity_point_test.cpp | 30 +++++++----- 2 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/core/include/mp-units/framework/value_cast.h b/src/core/include/mp-units/framework/value_cast.h index d3fb7445f..4a2d55ce6 100644 --- a/src/core/include/mp-units/framework/value_cast.h +++ b/src/core/include/mp-units/framework/value_cast.h @@ -231,10 +231,51 @@ template std::constructible_from::rep> [[nodiscard]] constexpr QuantityPoint auto value_cast(QP&& qp) { - return quantity_point{ - value_cast(std::forward(qp).quantity_from_origin_is_an_implementation_detail_), - std::remove_reference_t::point_origin} - .point_for(ToQP::point_origin); + using qp_type = std::remove_reference_t; + if constexpr (is_same_v, std::remove_const_t>) { + return quantity_point{ + value_cast(std::forward(qp).quantity_from(qp_type::point_origin)), + qp_type::point_origin}; + } else { + // it's unclear how hard we should try to avoid truncation here. For now, the only corner case we cater for, + // is when the range of the quantity type of at most one of QP or ToQP doesn't cover the offset between the + // point origins. In that case, we need to be careful to ensure we use the quantity type with the larger range + // of the two to perform the point_origin conversion. + // Numerically, we'll potentially need to do three things: + // (a) cast the representation type + // (b) scale the numerical value + // (c) add/subtract the origin difference + // In the following, we carefully select the order of these three operations: each of (a) and (b) is scheduled + // either before or after (c), such that (c) acts on the largest range possible among all combination of source + // and target unit and represenation. + constexpr Magnitude auto c_mag = get_canonical_unit(qp_type::unit).mag / get_canonical_unit(ToQP::unit).mag; + constexpr Magnitude auto num = detail::numerator(c_mag); + constexpr Magnitude auto den = detail::denominator(c_mag); + constexpr Magnitude auto irr = c_mag * (den / num); + using c_rep_type = detail::maybe_common_type; + using c_mag_type = detail::common_magnitude_type; + using multiplier_type = conditional< + treat_as_floating_point, + // ensure that the multiplier is also floating-point + conditional>, + // reuse user's type if possible + std::common_type_t>, std::common_type_t>, + c_mag_type>; + constexpr auto val = [](Magnitude auto m) { return get_value(m); }; + if constexpr (val(num) * val(irr) > val(den)) { + // original unit had a larger unit magnitude; if we first convert to the common representation but retain the + // unit, we obtain the largest possible range while not causing truncation of fractional values. This is optimal + // for the offset computation. + return value_cast( + value_cast(std::forward(qp)).point_for(ToQP::point_origin)); + } else { + // new unit may have a larger unit magnitude; we first need to convert to the new unit (potentially causing + // truncation, but no more than if we did the conversion later), but make sure we keep the larger of the two + // representation types. Then, we can perform the offset computation. + return value_cast( + value_cast(std::forward(qp)).point_for(ToQP::point_origin)); + } + } } diff --git a/test/static/quantity_point_test.cpp b/test/static/quantity_point_test.cpp index d03d35758..68518bf41 100644 --- a/test/static/quantity_point_test.cpp +++ b/test/static/quantity_point_test.cpp @@ -1695,21 +1695,25 @@ static_assert(value_cast(lvalue_qp).quantity_from_zero().numerical_val static_assert(value_cast>(quantity_point{2000 * m}).quantity_from_zero().numerical_value_in(km) == 2); static_assert(value_cast>(quantity_point{2000 * m}).quantity_from_zero().numerical_value_in(km) == 2); -static_assert( - !requires(quantity_point qp) { value_cast>(qp); }, - "value_cast shall not cast between different quantity types"); -static_assert( - !requires(quantity_point qp) { value_cast>(qp); }, - "value_cast shall not cast between different quantity types"); -static_assert(value_cast>(quantity_point{2 * km}) - .quantity_ref_from(mean_sea_level) + +template +constexpr bool value_cast_is_forbidden() +{ + // it appears we cannot have the requires clause right inside static_assert + return !requires(FromQ q) { value_cast(q); }; +} +static_assert(value_cast_is_forbidden, quantity_point>(), + "value_cast shall not cast between different quantity types"); +static_assert(value_cast_is_forbidden, quantity_point>(), + "value_cast shall not cast between different quantity types"); +static_assert(value_cast>(quantity_point{2 * isq::height[km], ground_level}) + .quantity_from_origin_is_an_implementation_detail_ .numerical_value_in(m) == 2042); -static_assert(value_cast>(quantity_point{ - std::int8_t{100} * mm}) - .quantity_ref_from(mean_sea_level) +static_assert(value_cast>(quantity_point{std::int8_t{100} * isq::height[mm], ground_level}) + .quantity_from_origin_is_an_implementation_detail_ .numerical_value_in(cm) == 4210); -static_assert(value_cast>(quantity_point{4210 * cm}) - .quantity_ref_from(ground_level) +static_assert(value_cast>(quantity_point{4210 * isq::height[cm], mean_sea_level}) + .quantity_from_origin_is_an_implementation_detail_ .numerical_value_in(mm) == 100); From f30fac17f0559e28b955380c1bf2337514290ea9 Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Sun, 12 May 2024 11:13:00 +0200 Subject: [PATCH 3/7] added a bit of documentation --- CHANGELOG.md | 1 + .../framework_basics/value_conversions.md | 14 ++++++++++++++ .../include/mp-units/framework/value_cast.h | 3 ++- test/static/quantity_point_test.cpp | 18 +++++++++--------- 4 files changed, 26 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efe2a403d..0fa00699f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - feat: `fma`, `isfinite`, `isinf`, and `isnan` math function added by [@NAThompson](https://github.com/NAThompson) - feat: `quantity_point` support added for `quantity_cast` and `value_cast` - feat: `value_cast` added +- feat: `value_cast(q)`, `value_cast(qp)` and `value_cast(qp)` added by [@burnpanck](https://github.com/burnpanck) - feat: `interconvertible(QuantitySpec, QuantitySpec)` added - feat: `qp.quantity_from_zero()` added - feat: `underlying_type` type trait added diff --git a/docs/users_guide/framework_basics/value_conversions.md b/docs/users_guide/framework_basics/value_conversions.md index bed6e9288..a296da3f4 100644 --- a/docs/users_guide/framework_basics/value_conversions.md +++ b/docs/users_guide/framework_basics/value_conversions.md @@ -147,3 +147,17 @@ using namespace unit_symbols; Price price{12.95 * USD}; Scaled spx = value_cast(price); ``` + +As a shortcut, instead of providing a unit and a representation type to `value_cast`, you may also provide a +`Quantity` type directly, from which unit and representation type are taken. However, `value_cast`, +still only allows for changes in unit and representation type, but not changing the type of the quantity. +For that, you will have to use a `quantity_cast` instead. + +Overloads are also provided for instances of `quantity_point`. Furthermore, in that case, there is +an overload `value_cast(qp)`, which is roughly equivalent to +`value_cast(qp).point_for(ToQP::point_origin)`. +In contrast to a separate `value_cast` followed by `point_for` (or vice-versa), the combined +`value_cast` tries to choose the order of the individual conversion steps in such a way, +to avoid both overflow and unnecessary loss of precision. Overflow is a risk because the change of origin point +may require an addition of a potentially large offset (the difference between the origin points), +which may well be outside the range of one or both quantity types. diff --git a/src/core/include/mp-units/framework/value_cast.h b/src/core/include/mp-units/framework/value_cast.h index 4a2d55ce6..a1ca34182 100644 --- a/src/core/include/mp-units/framework/value_cast.h +++ b/src/core/include/mp-units/framework/value_cast.h @@ -232,7 +232,8 @@ template [[nodiscard]] constexpr QuantityPoint auto value_cast(QP&& qp) { using qp_type = std::remove_reference_t; - if constexpr (is_same_v, std::remove_const_t>) { + if constexpr (is_same_v, + std::remove_const_t>) { return quantity_point{ value_cast(std::forward(qp).quantity_from(qp_type::point_origin)), qp_type::point_origin}; diff --git a/test/static/quantity_point_test.cpp b/test/static/quantity_point_test.cpp index 68518bf41..7c0e001c9 100644 --- a/test/static/quantity_point_test.cpp +++ b/test/static/quantity_point_test.cpp @@ -1706,15 +1706,15 @@ static_assert(value_cast_is_forbidden, quantity_point, quantity_point>(), "value_cast shall not cast between different quantity types"); -static_assert(value_cast>(quantity_point{2 * isq::height[km], ground_level}) - .quantity_from_origin_is_an_implementation_detail_ - .numerical_value_in(m) == 2042); -static_assert(value_cast>(quantity_point{std::int8_t{100} * isq::height[mm], ground_level}) - .quantity_from_origin_is_an_implementation_detail_ - .numerical_value_in(cm) == 4210); -static_assert(value_cast>(quantity_point{4210 * isq::height[cm], mean_sea_level}) - .quantity_from_origin_is_an_implementation_detail_ - .numerical_value_in(mm) == 100); +static_assert(value_cast>(quantity_point{2 * isq::height[km], + ground_level}) + .quantity_from_origin_is_an_implementation_detail_.numerical_value_in(m) == 2042); +static_assert(value_cast>( + quantity_point{std::int8_t{100} * isq::height[mm], ground_level}) + .quantity_from_origin_is_an_implementation_detail_.numerical_value_in(cm) == 4210); +static_assert(value_cast>( + quantity_point{4210 * isq::height[cm], mean_sea_level}) + .quantity_from_origin_is_an_implementation_detail_.numerical_value_in(mm) == 100); } // namespace From ae912c0efbf50dfd450661b7bd065492a0af447c Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Sun, 12 May 2024 11:47:03 +0200 Subject: [PATCH 4/7] added one more test; highlighting an issue with detail::common_magnitude_type_impl when both types are the same --- test/static/quantity_point_test.cpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/static/quantity_point_test.cpp b/test/static/quantity_point_test.cpp index 7c0e001c9..ef2520d16 100644 --- a/test/static/quantity_point_test.cpp +++ b/test/static/quantity_point_test.cpp @@ -1706,9 +1706,21 @@ static_assert(value_cast_is_forbidden, quantity_point, quantity_point>(), "value_cast shall not cast between different quantity types"); +// value_cast which does not touch the point_origin +static_assert(value_cast>(quantity_point{2 * isq::height[km]}) + .quantity_from_origin_is_an_implementation_detail_.numerical_value_in(m) == 2000); +static_assert(value_cast>(quantity_point{2000 * isq::height[m]}) + .quantity_from_origin_is_an_implementation_detail_.numerical_value_in(km) == 2); +// a value_cast which includes a change to the point origin +static_assert(value_cast>(quantity_point{2000 * isq::height[m], + ground_level}) + .quantity_from_origin_is_an_implementation_detail_.numerical_value_in(m) == 2042); +// a value_cast which includes a change to the point origin as-well as a change in units static_assert(value_cast>(quantity_point{2 * isq::height[km], ground_level}) .quantity_from_origin_is_an_implementation_detail_.numerical_value_in(m) == 2042); +// a value_cast which changes all three of unit, rep, point_origin simultaneously, and the range of either FromQP or +// ToQP does not include the other's point_origin static_assert(value_cast>( quantity_point{std::int8_t{100} * isq::height[mm], ground_level}) .quantity_from_origin_is_an_implementation_detail_.numerical_value_in(cm) == 4210); From 1e287c2a27f5ca77063d57d974a9b6102b3fd5e5 Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Sun, 12 May 2024 11:53:26 +0200 Subject: [PATCH 5/7] fixed value_cast with matching units but differing point_origin --- src/core/include/mp-units/framework/magnitude.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/include/mp-units/framework/magnitude.h b/src/core/include/mp-units/framework/magnitude.h index 1653ad128..c7ec64db6 100644 --- a/src/core/include/mp-units/framework/magnitude.h +++ b/src/core/include/mp-units/framework/magnitude.h @@ -767,7 +767,7 @@ template template [[nodiscard]] consteval auto common_magnitude_type_impl(magnitude) { - return (... * decltype(get_base_value(Ms)){}) * std::intmax_t{}; + return (decltype(get_base_value(Ms)){} * ... * std::intmax_t{}); } // Returns the most precise type to express the magnitude factor From 2459409caa005b136e742a87887a52c7ce28f599 Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Sat, 1 Jun 2024 20:09:29 +0200 Subject: [PATCH 6/7] created sudo_cast overload, and merged shared computation into separate helper --- src/core/include/mp-units/bits/sudo_cast.h | 120 +++++++++++++++--- .../include/mp-units/framework/value_cast.h | 62 ++------- 2 files changed, 114 insertions(+), 68 deletions(-) diff --git a/src/core/include/mp-units/bits/sudo_cast.h b/src/core/include/mp-units/bits/sudo_cast.h index 322423b4a..0444f93e4 100644 --- a/src/core/include/mp-units/bits/sudo_cast.h +++ b/src/core/include/mp-units/bits/sudo_cast.h @@ -37,6 +37,45 @@ template using maybe_common_type = MP_UNITS_TYPENAME std::conditional_t; }, get_common_type, std::type_identity>::type; +/** + * @brief Details about the conversion from one quantity to another. + * + * This struct calculates the conversion factor that needs to be applied to a number, + * in order to convert from one quantity to another. In addition to that, it also + * helps to determine what representations to use at which step in the conversion process, + * in order to avoid overflow and underflow while not causing excessive computations. + * + * @note This is a low-level facility. + * + * @tparam To a target quantity type to cast to + * @tparam From a source quantity type to cast from + */ +template + requires(castable(From::quantity_spec, To::quantity_spec)) +struct magnitude_conversion_traits { + // scale the number + static constexpr Magnitude auto c_mag = get_canonical_unit(From::unit).mag / get_canonical_unit(To::unit).mag; + static constexpr Magnitude auto num = numerator(c_mag); + static constexpr Magnitude auto den = denominator(c_mag); + static constexpr Magnitude auto irr = c_mag * (den / num); + using c_rep_type = maybe_common_type::rep, typename To::rep>; + using c_mag_type = common_magnitude_type; + using multiplier_type = conditional< + treat_as_floating_point, + // ensure that the multiplier is also floating-point + conditional>, + // reuse user's type if possible + std::common_type_t>, std::common_type_t>, + c_mag_type>; + using c_type = maybe_common_type; + static constexpr auto val(Magnitude auto m) { return get_value(m); }; + static constexpr multiplier_type num_mult = val(num); + static constexpr multiplier_type den_mult = val(den); + static constexpr multiplier_type irr_mult = val(irr); + static constexpr multiplier_type ratio = num_mult / den_mult * irr_mult; +}; + + /** * @brief Explicit cast between different quantity types * @@ -64,34 +103,77 @@ template // warnings on conversions } else { // scale the number - constexpr Magnitude auto c_mag = get_canonical_unit(q_unit).mag / get_canonical_unit(To::unit).mag; - constexpr Magnitude auto num = numerator(c_mag); - constexpr Magnitude auto den = denominator(c_mag); - constexpr Magnitude auto irr = c_mag * (den / num); - using c_rep_type = maybe_common_type::rep, typename To::rep>; - using c_mag_type = common_magnitude_type; - using multiplier_type = conditional< - treat_as_floating_point, - // ensure that the multiplier is also floating-point - conditional>, - // reuse user's type if possible - std::common_type_t>, std::common_type_t>, - c_mag_type>; - using c_type = maybe_common_type; - constexpr auto val = [](Magnitude auto m) { return get_value(m); }; - if constexpr (std::is_floating_point_v) { + using traits = magnitude_conversion_traits>; + if constexpr (std::is_floating_point_v) { // this results in great assembly - constexpr auto ratio = val(num) / val(den) * val(irr); auto res = static_cast( - static_cast(q.numerical_value_is_an_implementation_detail_) * ratio); + static_cast(q.numerical_value_is_an_implementation_detail_) * traits::ratio); return {res, To::reference}; } else { // this is slower but allows conversions like 2000 m -> 2 km without loosing data auto res = static_cast( - static_cast(q.numerical_value_is_an_implementation_detail_) * val(num) / val(den) * val(irr)); + static_cast(q.numerical_value_is_an_implementation_detail_) * traits::num_mult / + traits::den_mult * traits::irr_mult); return {res, To::reference}; } } } + +/** + * @brief Explicit cast between different quantity_point types + * + * @note This is a low-level facility and is too powerful to be used by the users directly. They should either use + * `value_cast` or `quantity_cast`. + * + * @tparam ToQP a target quantity point type to which to cast to + */ +template + requires QuantityPoint> && + (castable(std::remove_reference_t::quantity_spec, ToQP::quantity_spec)) && + (detail::same_absolute_point_origins(ToQP::point_origin, std::remove_reference_t::point_origin)) && + ((std::remove_reference_t::unit == ToQP::unit && + std::constructible_from::rep>) || + (std::remove_reference_t::unit != ToQP::unit)) +[[nodiscard]] constexpr QuantityPoint auto sudo_cast(FromQP&& qp) +{ + using qp_type = std::remove_reference_t; + if constexpr (is_same_v, + std::remove_const_t>) { + return quantity_point{ + sudo_cast(std::forward(qp).quantity_from(qp_type::point_origin)), + qp_type::point_origin}; + } else { + // it's unclear how hard we should try to avoid truncation here. For now, the only corner case we cater for, + // is when the range of the quantity type of at most one of QP or ToQP doesn't cover the offset between the + // point origins. In that case, we need to be careful to ensure we use the quantity type with the larger range + // of the two to perform the point_origin conversion. + // Numerically, we'll potentially need to do three things: + // (a) cast the representation type + // (b) scale the numerical value + // (c) add/subtract the origin difference + // In the following, we carefully select the order of these three operations: each of (a) and (b) is scheduled + // either before or after (c), such that (c) acts on the largest range possible among all combination of source + // and target unit and represenation. + using traits = magnitude_conversion_traits; + using c_rep_type = typename traits::c_rep_type; + if constexpr (traits::num_mult * traits::irr_mult > traits::den_mult) { + // original unit had a larger unit magnitude; if we first convert to the common representation but retain the + // unit, we obtain the largest possible range while not causing truncation of fractional values. This is optimal + // for the offset computation. + return sudo_cast( + sudo_cast>(std::forward(qp)) + .point_for(ToQP::point_origin)); + } else { + // new unit may have a larger unit magnitude; we first need to convert to the new unit (potentially causing + // truncation, but no more than if we did the conversion later), but make sure we keep the larger of the two + // representation types. Then, we can perform the offset computation. + return sudo_cast(sudo_cast>(std::forward(qp)) + .point_for(ToQP::point_origin)); + } + } +} + + } // namespace mp_units::detail diff --git a/src/core/include/mp-units/framework/value_cast.h b/src/core/include/mp-units/framework/value_cast.h index a1ca34182..6da41b2dc 100644 --- a/src/core/include/mp-units/framework/value_cast.h +++ b/src/core/include/mp-units/framework/value_cast.h @@ -185,10 +185,9 @@ template * (e.g. non-truncating) conversion. In truncating cases an explicit cast have to be used. * * inline constexpr struct A : absolute_point_origin A; - * inline constexpr struct B : relative_point_origin B; * - * using ToQP = quantity_point; - * auto qp = value_cast(quantity_point{1.23 * m}); + * using ToQ = quantity; + * auto qp = value_cast(quantity_point{1.23 * m}); * * Note that value_cast only changes the "representation aspects" (unit and representation * type), but not the "meaning" (quantity type or the actual point that is being described). @@ -221,6 +220,16 @@ template * type and point origin), but not the "meaning" (quantity type or the actual point that is * being described). * + * Note also that changing the point origin bears risks regarding truncation and overflow + * similar to other casts that change representation (which is why we require a `value_cast` + * and disallow implicit conversions). This cast is guaranteed not to cause overflow of + * any intermediate representation type provided that the input quantity point is within + * the range of `ToQP`. Calling `value_cast(qp)` on a `qp` outside of the range of `ToQP` + * is potentially undefined behaviour. + * The implementation further attempts not to cause more than + * rounding error than approximately the sum of the resolution of `qp` as represented in `FromQP`, + * plust the resolution of `qp` as represented in `ToQP`. + * * @tparam ToQP a target quantity point type to which to cast the representation of the point */ template @@ -231,52 +240,7 @@ template std::constructible_from::rep> [[nodiscard]] constexpr QuantityPoint auto value_cast(QP&& qp) { - using qp_type = std::remove_reference_t; - if constexpr (is_same_v, - std::remove_const_t>) { - return quantity_point{ - value_cast(std::forward(qp).quantity_from(qp_type::point_origin)), - qp_type::point_origin}; - } else { - // it's unclear how hard we should try to avoid truncation here. For now, the only corner case we cater for, - // is when the range of the quantity type of at most one of QP or ToQP doesn't cover the offset between the - // point origins. In that case, we need to be careful to ensure we use the quantity type with the larger range - // of the two to perform the point_origin conversion. - // Numerically, we'll potentially need to do three things: - // (a) cast the representation type - // (b) scale the numerical value - // (c) add/subtract the origin difference - // In the following, we carefully select the order of these three operations: each of (a) and (b) is scheduled - // either before or after (c), such that (c) acts on the largest range possible among all combination of source - // and target unit and represenation. - constexpr Magnitude auto c_mag = get_canonical_unit(qp_type::unit).mag / get_canonical_unit(ToQP::unit).mag; - constexpr Magnitude auto num = detail::numerator(c_mag); - constexpr Magnitude auto den = detail::denominator(c_mag); - constexpr Magnitude auto irr = c_mag * (den / num); - using c_rep_type = detail::maybe_common_type; - using c_mag_type = detail::common_magnitude_type; - using multiplier_type = conditional< - treat_as_floating_point, - // ensure that the multiplier is also floating-point - conditional>, - // reuse user's type if possible - std::common_type_t>, std::common_type_t>, - c_mag_type>; - constexpr auto val = [](Magnitude auto m) { return get_value(m); }; - if constexpr (val(num) * val(irr) > val(den)) { - // original unit had a larger unit magnitude; if we first convert to the common representation but retain the - // unit, we obtain the largest possible range while not causing truncation of fractional values. This is optimal - // for the offset computation. - return value_cast( - value_cast(std::forward(qp)).point_for(ToQP::point_origin)); - } else { - // new unit may have a larger unit magnitude; we first need to convert to the new unit (potentially causing - // truncation, but no more than if we did the conversion later), but make sure we keep the larger of the two - // representation types. Then, we can perform the offset computation. - return value_cast( - value_cast(std::forward(qp)).point_for(ToQP::point_origin)); - } - } + return detail::sudo_cast(std::forward(qp)); } From da17b01793520a70673862f194ed64e34b06e96c Mon Sep 17 00:00:00 2001 From: Yves Delley Date: Mon, 3 Jun 2024 20:43:23 +0200 Subject: [PATCH 7/7] added a bit more detail to the documentation of the quantity-point overloads of --- .../framework_basics/value_conversions.md | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/users_guide/framework_basics/value_conversions.md b/docs/users_guide/framework_basics/value_conversions.md index a296da3f4..8b57100dd 100644 --- a/docs/users_guide/framework_basics/value_conversions.md +++ b/docs/users_guide/framework_basics/value_conversions.md @@ -153,11 +153,25 @@ As a shortcut, instead of providing a unit and a representation type to `value_c still only allows for changes in unit and representation type, but not changing the type of the quantity. For that, you will have to use a `quantity_cast` instead. -Overloads are also provided for instances of `quantity_point`. Furthermore, in that case, there is -an overload `value_cast(qp)`, which is roughly equivalent to -`value_cast(qp).point_for(ToQP::point_origin)`. +Overloads are also provided for instances of `quantity_point`. +All variants of `value_cast<...>(q)` that apply to instances of `quantity` +have a corresponding version applicable to `quantity_point`, where the `point_origin` remains untouched, +and the cast changes how the "offset" from the origin is represented. +Specifically, for any `quantity_point` instance `qp`, all of the following equivalences hold: +```cpp +static_assert( value_cast(qp) == quantity_point{value_cast(qp.quantity_from(qp.point_origin)), qp.point_origin} ); +static_assert( value_cast(qp) == quantity_point{value_cast(qp.quantity_from(qp.point_origin)), qp.point_origin} ); +static_assert( value_cast(qp) == quantity_point{value_cast(qp.quantity_from(qp.point_origin)), qp.point_origin} ); +static_assert( value_cast(qp) == quantity_point{value_cast(qp.quantity_from(qp.point_origin)), qp.point_origin} ); +``` + +Furthermore, there is one additional overload `value_cast(qp)`. +This overload permits to additionally replace the `point_origin` with another compatible one, +while still representing the same point in the affine space. +Thus, it is roughly equivalent to +`value_cast(qp).point_for(ToQP::point_origin)`. In contrast to a separate `value_cast` followed by `point_for` (or vice-versa), the combined -`value_cast` tries to choose the order of the individual conversion steps in such a way, +`value_cast` tries to choose the order of the individual conversion steps in a way to avoid both overflow and unnecessary loss of precision. Overflow is a risk because the change of origin point may require an addition of a potentially large offset (the difference between the origin points), which may well be outside the range of one or both quantity types.