diff --git a/src/core/include/mp-units/bits/sudo_cast.h b/src/core/include/mp-units/bits/sudo_cast.h index 322423b4..0444f93e 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 a1ca3418..6da41b2d 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)); }