diff --git a/src/core/include/mp-units/bits/sudo_cast.h b/src/core/include/mp-units/bits/sudo_cast.h index fd7a9973..d6f6c841 100644 --- a/src/core/include/mp-units/bits/sudo_cast.h +++ b/src/core/include/mp-units/bits/sudo_cast.h @@ -31,35 +31,25 @@ namespace mp_units::detail { template -struct get_common_type : std::common_type {}; - -template -using maybe_common_type = MP_UNITS_TYPENAME std::conditional_t; }, - get_common_type, std::type_identity>::type; +using maybe_common_type = std::conditional_t; }, + std::common_type, std::type_identity>::type; /** - * @brief Details about the conversion from one quantity to another. + * @brief Type-related 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, + * This trait 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 + * @tparam M common magnitude between the two quantities + * @tparam Rep1 first quantity representation type + * @tparam Rep2 second quantity representation type */ -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; +template +struct conversion_type_traits { + using c_rep_type = maybe_common_type; + using c_mag_type = common_magnitude_type; using multiplier_type = conditional< treat_as_floating_point, // ensure that the multiplier is also floating-point @@ -68,11 +58,28 @@ struct magnitude_conversion_traits { 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 Value-related details about the conversion from one quantity to another + * + * This trait provide ingredients to calculate the conversion factor that needs to be applied + * to a number, in order to convert from one quantity to another. + * + * @note This is a low-level facility. + * + * @tparam M common magnitude between the two quantities + * @tparam T common multiplier representation type + */ +template +struct conversion_value_traits { + static constexpr Magnitude auto num = numerator(M); + static constexpr Magnitude auto den = denominator(M); + static constexpr Magnitude auto irr = M * (den / num); + static constexpr T num_mult = get_value(num); + static constexpr T den_mult = get_value(den); + static constexpr T irr_mult = get_value(irr); + static constexpr T ratio = num_mult / den_mult * irr_mult; }; @@ -84,35 +91,43 @@ struct magnitude_conversion_traits { * * @tparam To a target quantity type to cast to */ -template - requires Quantity> && - (castable(std::remove_reference_t::quantity_spec, To::quantity_spec)) && - ((std::remove_reference_t::unit == To::unit && - std::constructible_from::rep>) || - (std::remove_reference_t::unit != To::unit)) // && scalable_with_)) +template> + requires Quantity && (castable(From::quantity_spec, To::quantity_spec)) && + ((From::unit == To::unit && std::constructible_from) || + (From::unit != To::unit)) // && scalable_with_)) // TODO how to constrain the second part here? -[[nodiscard]] constexpr To sudo_cast(From&& q) +[[nodiscard]] constexpr To sudo_cast(FwdFrom&& q) { - constexpr auto q_unit = std::remove_reference_t::unit; + constexpr auto q_unit = From::unit; if constexpr (q_unit == To::unit) { // no scaling of the number needed - return {static_cast(std::forward(q).numerical_value_is_an_implementation_detail_), + return {static_cast(std::forward(q).numerical_value_is_an_implementation_detail_), To::reference}; // this is the only (and recommended) way to do a truncating conversion on a number, so we // are using static_cast to suppress all the compiler warnings on conversions } else { + static constexpr Magnitude auto c_mag = get_canonical_unit(From::unit).mag / get_canonical_unit(To::unit).mag; + using type_traits = conversion_type_traits; + using multiplier_type = typename type_traits::multiplier_type; + auto scale = [&](std::invocable auto func) { + auto res = + static_cast(func(static_cast(q.numerical_value_is_an_implementation_detail_))); + return To{res, To::reference}; + }; + // scale the number - using traits = magnitude_conversion_traits>; - if constexpr (std::is_floating_point_v) { - // this results in great assembly - auto res = static_cast( - 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_) * traits::num_mult / - traits::den_mult * traits::irr_mult); - return {res, To::reference}; + if constexpr (is_integral(c_mag)) + return scale([&](auto value) { return value * get_value(numerator(c_mag)); }); + else if constexpr (is_integral(pow<-1>(c_mag))) + return scale([&](auto value) { return value / get_value(denominator(c_mag)); }); + else { + using value_traits = conversion_value_traits; + if constexpr (std::is_floating_point_v) + // this results in great assembly + return scale([](auto value) { return value * value_traits::ratio; }); + else + // this is slower but allows conversions like 2000 m -> 2 km without loosing data + return scale( + [](auto value) { return value * value_traits::num_mult / value_traits::den_mult * value_traits::irr_mult; }); } } } @@ -126,21 +141,18 @@ template * * @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) +template> + requires QuantityPoint && (castable(FromQP::quantity_spec, ToQP::quantity_spec)) && + (detail::same_absolute_point_origins(ToQP::point_origin, FromQP::point_origin)) && + ((FromQP::unit == ToQP::unit && std::constructible_from) || + (FromQP::unit != ToQP::unit)) +[[nodiscard]] constexpr QuantityPoint auto sudo_cast(FwdFromQP&& qp) { - using qp_type = std::remove_reference_t; if constexpr (is_same_v, - std::remove_const_t>) { + std::remove_const_t>) { return quantity_point{ - sudo_cast(std::forward(qp).quantity_from(qp_type::point_origin)), - qp_type::point_origin}; + sudo_cast(std::forward(qp).quantity_from(FromQP::point_origin)), + FromQP::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 @@ -152,23 +164,26 @@ template // (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) { + // and target unit and representation. + static constexpr Magnitude auto c_mag = get_canonical_unit(FromQP::unit).mag / get_canonical_unit(ToQP::unit).mag; + using type_traits = conversion_type_traits; + using value_traits = conversion_value_traits; + using c_rep_type = typename type_traits::c_rep_type; + if constexpr (value_traits::num_mult * value_traits::irr_mult > value_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)) + 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)); + return sudo_cast( + sudo_cast>( + std::forward(qp)) + .point_for(ToQP::point_origin)); } } } diff --git a/test/static/quantity_test.cpp b/test/static/quantity_test.cpp index 63658896..caba5705 100644 --- a/test/static/quantity_test.cpp +++ b/test/static/quantity_test.cpp @@ -260,6 +260,9 @@ static_assert(quantity(2 * km).force_in(km).numerical_valu static_assert(quantity(2 * km).force_in(m).numerical_value_in(m) == 2000); static_assert(quantity(2000 * m).force_in(km).numerical_value_in(km) == 2); +static_assert((15. * m).in(nm).numerical_value_in(m) == 15.); +static_assert((15'000. * nm).in(m).numerical_value_in(nm) == 15'000.); + template typename Q> concept invalid_unit_conversion = requires { requires !requires { Q(2000 * m).in(km); }; // truncating conversion