diff --git a/CHANGELOG.md b/CHANGELOG.md index bc6b3e3bb..cc38d8954 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - feat: `fma` for quantity points added - 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: `value_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 5e0912060..bb9d6559d 100644 --- a/docs/users_guide/framework_basics/value_conversions.md +++ b/docs/users_guide/framework_basics/value_conversions.md @@ -147,3 +147,31 @@ 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`. +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 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/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 1f92bceaa..6da41b2dc 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,70 @@ 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; + * + * 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). + * + * @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). + * + * 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 + 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 detail::sudo_cast(std::forward(qp)); +} + + } // namespace mp_units diff --git a/test/static/quantity_point_test.cpp b/test/static/quantity_point_test.cpp index 189ab576e..a335ccac4 100644 --- a/test/static/quantity_point_test.cpp +++ b/test/static/quantity_point_test.cpp @@ -1702,4 +1702,41 @@ 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); + +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"); +// 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); +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