Skip to content

Commit

Permalink
Add as_raw_number(...) utility (#333)
Browse files Browse the repository at this point in the history
This lets us pave the way to change the behavior of quantity arithmetic
when all units cancel out (#185).  If we make this change all at once,
it'll generally be too big and hard to handle.  But if we provide this
utility now, then people can migrate individual callsites gradually over
time.  Then, at the end (probably 0.5.0?), making the switch to the new
behavior won't be such a big change.

Helps #185.
  • Loading branch information
chiphogg authored Dec 2, 2024
1 parent e88baac commit d450960
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 0 deletions.
14 changes: 14 additions & 0 deletions au/code/au/quantity.hh
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,20 @@ constexpr auto operator%(Quantity<U1, R1> q1, Quantity<U2, R2> q2) {
return make_quantity<U>(q1.in(U{}) % q2.in(U{}));
}

// Callsite-readable way to convert a `Quantity` to a raw number.
//
// Only works for dimensionless `Quantities`; will return a compile-time error otherwise.
//
// Identity for non-`Quantity` types.
template <typename U, typename R>
constexpr R as_raw_number(Quantity<U, R> q) {
return q.as(UnitProductT<>{});
}
template <typename T>
constexpr T as_raw_number(T x) {
return x;
}

// Type trait to detect whether two Quantity types are equivalent.
//
// In this library, Quantity types are "equivalent" exactly when they use the same Rep, and are
Expand Down
29 changes: 29 additions & 0 deletions au/code/au/quantity_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ static constexpr QuantityMaker<Meters> meters{};
static_assert(are_units_quantity_equivalent(Centi<Meters>{} * mag<254>(), Inches{} * mag<100>()),
"Double-check this ad hoc definition of meters");

struct Unos : decltype(UnitProductT<>{}) {};
constexpr auto unos = QuantityMaker<Unos>{};

struct Percent : decltype(Unos{} / mag<100>()) {};
constexpr auto percent = QuantityMaker<Percent>{};

struct Hours : UnitImpl<Time> {};
constexpr auto hour = SingularNameFor<Hours>{};
constexpr auto hours = QuantityMaker<Hours>{};
Expand All @@ -63,6 +69,12 @@ struct Minutes : decltype(Hours{} / mag<60>()) {};
constexpr auto minute = SingularNameFor<Minutes>{};
constexpr auto minutes = QuantityMaker<Minutes>{};

struct Seconds : decltype(Minutes{} / mag<60>()) {};
constexpr auto seconds = QuantityMaker<Seconds>{};

struct Hertz : decltype(inverse(Seconds{})) {};
constexpr auto hertz = QuantityMaker<Hertz>{};

struct Days : decltype(Hours{} * mag<24>()) {};
constexpr auto days = QuantityMaker<Days>{};

Expand Down Expand Up @@ -786,6 +798,23 @@ TEST(QuantityMaker, ProvidesAssociatedUnit) {
StaticAssertTypeEq<AssociatedUnitT<QuantityMaker<Hours>>, Hours>();
}

TEST(AsRawNumber, ExtractsRawNumberForUnitlessQuantity) {
EXPECT_THAT(as_raw_number(unos(3)), SameTypeAndValue(3));
EXPECT_THAT(as_raw_number(unos(3.1415f)), SameTypeAndValue(3.1415f));
}

TEST(AsRawNumber, PerformsConversionsWherePermissible) {
EXPECT_THAT(as_raw_number(percent(75.0)), SameTypeAndValue(0.75));
EXPECT_THAT(as_raw_number(kilo(hertz)(7) * seconds(3)), SameTypeAndValue(21'000));
}

TEST(AsRawNumber, IdentityForBuiltInNumericTypes) {
EXPECT_THAT(as_raw_number(3), SameTypeAndValue(3));
EXPECT_THAT(as_raw_number(3u), SameTypeAndValue(3u));
EXPECT_THAT(as_raw_number(3.1415), SameTypeAndValue(3.1415));
EXPECT_THAT(as_raw_number(3.1415f), SameTypeAndValue(3.1415f));
}

TEST(WillConversionOverflow, SensitiveToTypeBoundariesForPureIntegerMultiply) {
{
auto will_m_to_mm_overflow_i32 = [](int32_t x) {
Expand Down
28 changes: 28 additions & 0 deletions docs/reference/quantity.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,32 @@ These functions also support an explicit template parameter: so, `.coerce_as<T>(
Prefer **not** to use the "coercing versions" if possible, because you will get more safety
checks. The risks which the "base" versions warn about are real.

### Special case: dimensionless and unitless results {#as-raw-number}

Users may expect that the product of quantities such as `seconds` and `hertz` would completely
cancel out, and produce a raw, simple C++ numeric type. Currently, this is indeed the case, but we
have also found that it makes the library harder to reason about. Instead, we hope in the future to
return a `Quantity` type _consistently_ from arithmetical operations on `Quantity` inputs (see
[#185]).

In order to obtain that raw number robustly, both now and in the future, you can use the
`as_raw_number` function, a callsite-readable way to "exit" the library. This will also opt into
all mechanisms and safety features of the library. In particular:

- We will automatically perform all necessary conversions.
- This will not compile unless the input is _dimensionless_.
- If the conversion is dangerous (say, from `Quantity<Percent, int>`, which cannot in general be
represented exactly as a raw `int`, we will also fail to compile.

Users should get in the habit of using `as_raw_number` whenever they really want a raw number. This
communicates intent, and also works both before and after [#185] is implemented.

!!! example
```cpp
constexpr auto num_beats = as_raw_number(kilo(hertz)(7) * seconds(3));
// Result: 21'000 (of type `int`)
```

## Non-Type Template Parameters (NTTPs) {#nttp}

A _non-type template parameter_ (NTTP) is a template parameter that is not a _type_, but rather some
Expand Down Expand Up @@ -698,3 +724,5 @@ the following conditions hold.

- For _types_ `U1` and `U2`:
- `AreQuantityTypesEquivalent<U1, U2>::value`

[#185]: https://github.com/aurora-opensource/au/issues/185

0 comments on commit d450960

Please sign in to comment.