Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add as_raw_number(...) utility #333

Merged
merged 2 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commend: Thanks for this clarification. I was worried about cases like as_raw_number(seconds(1)).

//
// 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