Skip to content

Commit

Permalink
Merge pull request #571 from burnpanck/feature/more-value-casts
Browse files Browse the repository at this point in the history
implement value_cast<ToQ> and value_cast<ToQP>
  • Loading branch information
mpusz authored Jun 14, 2024
2 parents f49b4c6 + b113f6a commit 4ed4b23
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 23 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
- feat: `fma` for quantity points added
- feat: `quantity_point` support added for `quantity_cast` and `value_cast`
- feat: `value_cast<Unit, Representation>` added
- feat: `value_cast<Quantity>(q)`, `value_cast<Quantity>(qp)` and `value_cast<QuantityPoint>(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
Expand Down
28 changes: 28 additions & 0 deletions docs/users_guide/framework_basics/value_conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,31 @@ using namespace unit_symbols;
Price price{12.95 * USD};
Scaled spx = value_cast<USD_s, std::int64_t>(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<Quantity>`,
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<Rep>(qp) == quantity_point{value_cast<Rep>(qp.quantity_from(qp.point_origin)), qp.point_origin} );
static_assert( value_cast<U>(qp) == quantity_point{value_cast<U>(qp.quantity_from(qp.point_origin)), qp.point_origin} );
static_assert( value_cast<U, Rep>(qp) == quantity_point{value_cast<U, Rep>(qp.quantity_from(qp.point_origin)), qp.point_origin} );
static_assert( value_cast<Q>(qp) == quantity_point{value_cast<Q>(qp.quantity_from(qp.point_origin)), qp.point_origin} );
```

Furthermore, there is one additional overload `value_cast<ToQP>(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<ToQP::unit, ToQP::rep>(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.
120 changes: 101 additions & 19 deletions src/core/include/mp-units/bits/sudo_cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,45 @@ template<typename T, typename Other>
using maybe_common_type = MP_UNITS_TYPENAME std::conditional_t<requires { typename std::common_type_t<T, Other>; },
get_common_type<T, Other>, std::type_identity<T>>::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<Quantity To, Quantity From>
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<typename std::remove_reference_t<From>::rep, typename To::rep>;
using c_mag_type = common_magnitude_type<c_mag>;
using multiplier_type = conditional<
treat_as_floating_point<c_rep_type>,
// ensure that the multiplier is also floating-point
conditional<std::is_arithmetic_v<value_type_t<c_rep_type>>,
// reuse user's type if possible
std::common_type_t<c_mag_type, value_type_t<c_rep_type>>, std::common_type_t<c_mag_type, double>>,
c_mag_type>;
using c_type = maybe_common_type<c_rep_type, multiplier_type>;
static constexpr auto val(Magnitude auto m) { return get_value<multiplier_type>(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
*
Expand Down Expand Up @@ -64,34 +103,77 @@ template<Quantity To, typename From>
// 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<typename std::remove_reference_t<From>::rep, typename To::rep>;
using c_mag_type = common_magnitude_type<c_mag>;
using multiplier_type = conditional<
treat_as_floating_point<c_rep_type>,
// ensure that the multiplier is also floating-point
conditional<std::is_arithmetic_v<value_type_t<c_rep_type>>,
// reuse user's type if possible
std::common_type_t<c_mag_type, value_type_t<c_rep_type>>, std::common_type_t<c_mag_type, double>>,
c_mag_type>;
using c_type = maybe_common_type<c_rep_type, multiplier_type>;
constexpr auto val = [](Magnitude auto m) { return get_value<multiplier_type>(m); };
if constexpr (std::is_floating_point_v<multiplier_type>) {
using traits = magnitude_conversion_traits<To, std::remove_reference_t<From>>;
if constexpr (std::is_floating_point_v<typename traits::multiplier_type>) {
// this results in great assembly
constexpr auto ratio = val(num) / val(den) * val(irr);
auto res = static_cast<MP_UNITS_TYPENAME To::rep>(
static_cast<c_type>(q.numerical_value_is_an_implementation_detail_) * ratio);
static_cast<traits::c_type>(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<MP_UNITS_TYPENAME To::rep>(
static_cast<c_type>(q.numerical_value_is_an_implementation_detail_) * val(num) / val(den) * val(irr));
static_cast<traits::c_type>(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<QuantityPoint ToQP, typename FromQP>
requires QuantityPoint<std::remove_cvref_t<FromQP>> &&
(castable(std::remove_reference_t<FromQP>::quantity_spec, ToQP::quantity_spec)) &&
(detail::same_absolute_point_origins(ToQP::point_origin, std::remove_reference_t<FromQP>::point_origin)) &&
((std::remove_reference_t<FromQP>::unit == ToQP::unit &&
std::constructible_from<typename ToQP::rep, typename std::remove_reference_t<FromQP>::rep>) ||
(std::remove_reference_t<FromQP>::unit != ToQP::unit))
[[nodiscard]] constexpr QuantityPoint auto sudo_cast(FromQP&& qp)
{
using qp_type = std::remove_reference_t<FromQP>;
if constexpr (is_same_v<std::remove_const_t<decltype(ToQP::point_origin)>,
std::remove_const_t<decltype(qp_type::point_origin)>>) {
return quantity_point{
sudo_cast<typename ToQP::quantity_type>(std::forward<FromQP>(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<typename ToQP::quantity_type, typename qp_type::quantity_type>;
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<ToQP>(
sudo_cast<quantity_point<qp_type::reference, qp_type::point_origin, c_rep_type>>(std::forward<FromQP>(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<ToQP>(sudo_cast<quantity_point<make_reference(qp_type::quantity_spec, ToQP::unit),
qp_type::point_origin, c_rep_type>>(std::forward<FromQP>(qp))
.point_for(ToQP::point_origin));
}
}
}


} // namespace mp_units::detail
100 changes: 96 additions & 4 deletions src/core/include/mp-units/framework/value_cast.h
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ template<Representation ToRep, typename Q>
*
* auto q = value_cast<us, int>(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<Unit auto ToU, Representation ToRep, typename Q>
requires Quantity<std::remove_cvref_t<Q>> && (convertible(std::remove_reference_t<Q>::reference, ToU)) &&
Expand All @@ -92,6 +93,30 @@ template<Unit auto ToU, Representation ToRep, typename Q>
return detail::sudo_cast<quantity<detail::make_reference(q_type::quantity_spec, ToU), ToRep>>(std::forward<Q>(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<us, int>;
* auto q = value_cast<ToQ>(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<Quantity ToQ, typename Q>
requires Quantity<std::remove_cvref_t<Q>> && (convertible(std::remove_reference_t<Q>::reference, ToQ::unit)) &&
(ToQ::quantity_spec == std::remove_reference_t<Q>::quantity_spec) &&
std::constructible_from<typename ToQ::rep, typename std::remove_reference_t<Q>::rep>
[[nodiscard]] constexpr Quantity auto value_cast(Q&& q)
{
return detail::sudo_cast<ToQ>(std::forward<Q>(q));
}

/**
* @brief Explicit cast of a quantity point's unit
*
Expand Down Expand Up @@ -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<us, int>(1.23 * ms);
* auto qp = value_cast<us, int>(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<Unit auto ToU, Representation ToRep, typename QP>
requires QuantityPoint<std::remove_cvref_t<QP>> && (convertible(std::remove_reference_t<QP>::reference, ToU)) &&
Expand All @@ -152,4 +178,70 @@ template<Unit auto ToU, Representation ToRep, typename QP>
std::remove_reference_t<QP>::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, isq::distance> A;
*
* using ToQ = quantity<mm, int>;
* auto qp = value_cast<ToQ>(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<Quantity ToQ, typename QP>
requires QuantityPoint<std::remove_cvref_t<QP>> && (convertible(std::remove_reference_t<QP>::reference, ToQ::unit)) &&
(ToQ::quantity_spec == std::remove_reference_t<QP>::quantity_spec) &&
std::constructible_from<typename ToQ::rep, typename std::remove_reference_t<QP>::rep>
[[nodiscard]] constexpr QuantityPoint auto value_cast(QP&& qp)
{
return quantity_point{value_cast<ToQ>(std::forward<QP>(qp).quantity_from_origin_is_an_implementation_detail_),
std::remove_reference_t<QP>::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, isq::distance> A;
* inline constexpr struct B : relative_point_origin<A + 1*m> B;
*
* using ToQP = quantity_point<mm, B, int>;
* auto qp = value_cast<ToQP>(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<ToQP>(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<QuantityPoint ToQP, typename QP>
requires QuantityPoint<std::remove_cvref_t<QP>> &&
(convertible(std::remove_reference_t<QP>::reference, ToQP::unit)) &&
(ToQP::quantity_spec == std::remove_reference_t<QP>::quantity_spec) &&
(detail::same_absolute_point_origins(ToQP::point_origin, std::remove_reference_t<QP>::point_origin)) &&
std::constructible_from<typename ToQP::rep, typename std::remove_reference_t<QP>::rep>
[[nodiscard]] constexpr QuantityPoint auto value_cast(QP&& qp)
{
return detail::sudo_cast<ToQP>(std::forward<QP>(qp));
}


} // namespace mp_units
37 changes: 37 additions & 0 deletions test/static/quantity_point_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1702,4 +1702,41 @@ static_assert(value_cast<float>(lvalue_qp).quantity_from_zero().numerical_value_
static_assert(value_cast<m, float>(lvalue_qp).quantity_from_zero().numerical_value_in(m) == 2000.f);
} // namespace lvalue_tests

static_assert(value_cast<quantity<km, int>>(quantity_point{2000 * m}).quantity_from_zero().numerical_value_in(km) == 2);
static_assert(value_cast<quantity_point<km>>(quantity_point{2000 * m}).quantity_from_zero().numerical_value_in(km) ==
2);

template<typename ToQ, typename FromQ>
constexpr bool value_cast_is_forbidden()
{
// it appears we cannot have the requires clause right inside static_assert
return !requires(FromQ q) { value_cast<ToQ>(q); };
}
static_assert(value_cast_is_forbidden<quantity_point<m>, quantity_point<isq::width[m]>>(),
"value_cast shall not cast between different quantity types");
static_assert(value_cast_is_forbidden<quantity_point<isq::width[m]>, quantity_point<m>>(),
"value_cast shall not cast between different quantity types");
// value_cast which does not touch the point_origin
static_assert(value_cast<quantity_point<isq::height[m]>>(quantity_point{2 * isq::height[km]})
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(m) == 2000);
static_assert(value_cast<quantity_point<isq::height[km]>>(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<isq::height[m], mean_sea_level>>(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<isq::height[m], mean_sea_level>>(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<isq::height[cm], mean_sea_level, int>>(
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<isq::height[mm], ground_level, std::int8_t>>(
quantity_point{4210 * isq::height[cm], mean_sea_level})
.quantity_from_origin_is_an_implementation_detail_.numerical_value_in(mm) == 100);


} // namespace

0 comments on commit 4ed4b23

Please sign in to comment.