Skip to content

Commit

Permalink
Add coercing versions of conversion functions
Browse files Browse the repository at this point in the history
These are variants of `.as` and `.in` for both `Quantity` and
`QuantityPoint`.  When we precede these with the "coerce" vocabulary
word, they become forcing, ignoring the safety checks for truncation and
overflow.

This new syntax has two major advantages over the explicit-Rep version.
First, it makes the intent clearer.  Second, it stops forcing users to
repeat the rep when they want the same rep they started with, which I
think is the usual case.  (Thus, without these APIs, explicit-rep
obscures intent: one never knows whether the author wanted a cast, or
wanted to bypass the safety checks.)

It also unlocks another, later improvement: we will be able to extend
the safety checks to the explicit-rep versions!  But we won't attempt
that for a while because that's a breaking change.

There may be other APIs that would benefit from using the "coerce"
vocabulary word instead of explicit-rep.  However, we'll start with just
these, both because they're the overwhelmingly most common use case, and
because it gives us a chance to try out these ideas in practice for a
while.

Helps #122.

We'll start with just these functions because they're by far the most
prevalent.  Later on, we can
  • Loading branch information
chiphogg committed Aug 29, 2023
1 parent be0f010 commit 18a9afe
Show file tree
Hide file tree
Showing 10 changed files with 266 additions and 51 deletions.
2 changes: 1 addition & 1 deletion au/prefix_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ TEST(PrefixApplier, ConvertsQuantityMakerToMakerOfCorrespondingPrefixedType) {
constexpr auto d = inches(2);
EXPECT_THAT(d.in(make_milli(inches)), SameTypeAndValue(2'000));

EXPECT_THAT(make_milli(inches)(5'777).in<int>(inches), SameTypeAndValue(5));
EXPECT_THAT(make_milli(inches)(5'777).coerce_in(inches), SameTypeAndValue(5));
}

TEST(PrefixApplier, ConvertsSingularNameForToCorrespondingPrefixedType) {
Expand Down
22 changes: 22 additions & 0 deletions au/quantity.hh
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,28 @@ class Quantity {
return in<R>(U{});
}

// "Forcing" conversions, which explicitly ignore safety checks for overflow and truncation.
template <typename NewUnit>
constexpr auto coerce_as(NewUnit) const {
// Usage example: `q.coerce_as(new_units)`.
return as<Rep>(NewUnit{});
}
template <typename NewRep, typename NewUnit>
constexpr auto coerce_as(NewUnit) const {
// Usage example: `q.coerce_as<T>(new_units)`.
return as<NewRep>(NewUnit{});
}
template <typename NewUnit>
constexpr auto coerce_in(NewUnit) const {
// Usage example: `q.coerce_in(new_units)`.
return in<Rep>(NewUnit{});
}
template <typename NewRep, typename NewUnit>
constexpr auto coerce_in(NewUnit) const {
// Usage example: `q.coerce_in<T>(new_units)`.
return in<NewRep>(NewUnit{});
}

// Direct access to the underlying value member, with any Quantity-equivalent Unit.
//
// Mutable access, QuantityMaker input.
Expand Down
22 changes: 22 additions & 0 deletions au/quantity_point.hh
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,28 @@ class QuantityPoint {
return in<R>(U{});
}

// "Forcing" conversions, which explicitly ignore safety checks for overflow and truncation.
template <typename NewUnit>
constexpr auto coerce_as(NewUnit) const {
// Usage example: `p.coerce_as(new_units)`.
return as<Rep>(NewUnit{});
}
template <typename NewRep, typename NewUnit>
constexpr auto coerce_as(NewUnit) const {
// Usage example: `p.coerce_as<T>(new_units)`.
return as<NewRep>(NewUnit{});
}
template <typename NewUnit>
constexpr auto coerce_in(NewUnit) const {
// Usage example: `p.coerce_in(new_units)`.
return in<Rep>(NewUnit{});
}
template <typename NewRep, typename NewUnit>
constexpr auto coerce_in(NewUnit) const {
// Usage example: `p.coerce_in<T>(new_units)`.
return in<NewRep>(NewUnit{});
}

// Direct access to the underlying value member, with any Point-equivalent Unit.
//
// Mutable access, QuantityPointMaker input.
Expand Down
55 changes: 53 additions & 2 deletions au/quantity_point_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ struct Meters : UnitImpl<Length> {};
constexpr QuantityMaker<Meters> meters{};
constexpr QuantityPointMaker<Meters> meters_pt{};

struct Inches : decltype(Centi<Meters>{} * mag<254>() / mag<100>()) {};
constexpr auto inches_pt = QuantityPointMaker<Inches>{};

struct Feet : decltype(Inches{} * mag<12>()) {};
constexpr auto feet_pt = QuantityPointMaker<Feet>{};

struct Kelvins : UnitImpl<Temperature> {};
constexpr QuantityMaker<Kelvins> kelvins{};
constexpr QuantityPointMaker<Kelvins> kelvins_pt{};
Expand Down Expand Up @@ -174,14 +180,59 @@ TEST(QuantityPoint, CanRequestOutputRepWhenCallingIn) {
}

TEST(QuantityPoint, CanCastToUnitWithDifferentMagnitude) {
EXPECT_THAT(centi(meters_pt)(75).as<int>(meters_pt), SameTypeAndValue(meters_pt(0)));
EXPECT_THAT(centi(meters_pt)(75).coerce_as(meters_pt), SameTypeAndValue(meters_pt(0)));

EXPECT_THAT(centi(meters_pt)(75.0).as(meters_pt), SameTypeAndValue(meters_pt(0.75)));
}

TEST(QuantityPoint, CanCastToUnitWithDifferentOrigin) {
EXPECT_THAT(celsius_pt(10.).as(kelvins_pt), IsNear(kelvins_pt(283.15), nano(kelvins)(1)));
EXPECT_THAT(celsius_pt(10).as<int>(Kelvins{}), SameTypeAndValue(kelvins_pt(283)));
EXPECT_THAT(celsius_pt(10).coerce_as(Kelvins{}), SameTypeAndValue(kelvins_pt(283)));
}

TEST(QuantityPoint, CoerceAsWillForceLossyConversion) {
// Truncation.
EXPECT_THAT(inches_pt(30).coerce_as(feet_pt), SameTypeAndValue(feet_pt(2)));

// Unsigned overflow.
ASSERT_EQ(static_cast<uint8_t>(30 * 12), 104);
EXPECT_THAT(feet_pt(uint8_t{30}).coerce_as(inches_pt),
SameTypeAndValue(inches_pt(uint8_t{104})));
}

TEST(QuantityPoint, CoerceAsExplicitRepSetsOutputType) {
// Coerced truncation.
EXPECT_THAT(inches_pt(30).coerce_as<std::size_t>(feet_pt),
SameTypeAndValue(feet_pt(std::size_t{2})));

// Exact answer for floating point destination type.
EXPECT_THAT(inches_pt(30).coerce_as<float>(feet_pt), SameTypeAndValue(feet_pt(2.5f)));

// Coerced unsigned overflow.
ASSERT_EQ(static_cast<uint8_t>(30 * 12), 104);
EXPECT_THAT(feet_pt(30).coerce_as<uint8_t>(inches_pt),
SameTypeAndValue(inches_pt(uint8_t{104})));
}

TEST(QuantityPoint, CoerceInWillForceLossyConversion) {
// Truncation.
EXPECT_THAT(inches_pt(30).coerce_in(feet_pt), SameTypeAndValue(2));

// Unsigned overflow.
ASSERT_EQ(static_cast<uint8_t>(30 * 12), 104);
EXPECT_THAT(feet_pt(uint8_t{30}).coerce_in(inches_pt), SameTypeAndValue(uint8_t{104}));
}

TEST(QuantityPoint, CoerceInExplicitRepSetsOutputType) {
// Coerced truncation.
EXPECT_THAT(inches_pt(30).coerce_in<std::size_t>(feet_pt), SameTypeAndValue(std::size_t{2}));

// Exact answer for floating point destination type.
EXPECT_THAT(inches_pt(30).coerce_in<float>(feet_pt), SameTypeAndValue(2.5f));

// Coerced unsigned overflow.
ASSERT_EQ(static_cast<uint8_t>(30 * 12), 104);
EXPECT_THAT(feet_pt(30).coerce_in<uint8_t>(inches_pt), SameTypeAndValue(uint8_t{104}));
}

TEST(QuantityPoint, ComparisonsWorkAsExpected) {
Expand Down
53 changes: 48 additions & 5 deletions au/quantity_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,48 @@ TEST(Quantity, SupportsDirectConstAccessWithQuantityMakerOfEquivalentUnit) {
// static_cast<const void *>(&x));
}

TEST(Quantity, CoerceAsWillForceLossyConversion) {
// Truncation.
EXPECT_THAT(inches(30).coerce_as(feet), SameTypeAndValue(feet(2)));

// Unsigned overflow.
ASSERT_EQ(static_cast<uint8_t>(30 * 12), 104);
EXPECT_THAT(feet(uint8_t{30}).coerce_as(inches), SameTypeAndValue(inches(uint8_t{104})));
}

TEST(Quantity, CoerceAsExplicitRepSetsOutputType) {
// Coerced truncation.
EXPECT_THAT(inches(30).coerce_as<std::size_t>(feet), SameTypeAndValue(feet(std::size_t{2})));

// Exact answer for floating point destination type.
EXPECT_THAT(inches(30).coerce_as<float>(feet), SameTypeAndValue(feet(2.5f)));

// Coerced unsigned overflow.
ASSERT_EQ(static_cast<uint8_t>(30 * 12), 104);
EXPECT_THAT(feet(30).coerce_as<uint8_t>(inches), SameTypeAndValue(inches(uint8_t{104})));
}

TEST(Quantity, CoerceInWillForceLossyConversion) {
// Truncation.
EXPECT_THAT(inches(30).coerce_in(feet), SameTypeAndValue(2));

// Unsigned overflow.
ASSERT_EQ(static_cast<uint8_t>(30 * 12), 104);
EXPECT_THAT(feet(uint8_t{30}).coerce_in(inches), SameTypeAndValue(uint8_t{104}));
}

TEST(Quantity, CoerceInExplicitRepSetsOutputType) {
// Coerced truncation.
EXPECT_THAT(inches(30).coerce_in<std::size_t>(feet), SameTypeAndValue(std::size_t{2}));

// Exact answer for floating point destination type.
EXPECT_THAT(inches(30).coerce_in<float>(feet), SameTypeAndValue(2.5f));

// Coerced unsigned overflow.
ASSERT_EQ(static_cast<uint8_t>(30 * 12), 104);
EXPECT_THAT(feet(30).coerce_in<uint8_t>(inches), SameTypeAndValue(uint8_t{104}));
}

TEST(Quantity, CanImplicitlyConvertToDifferentUnitOfSameDimension) {
constexpr QuantityI32<Inches> x = yards(2);
EXPECT_EQ(x.in(inches), 72);
Expand Down Expand Up @@ -506,20 +548,20 @@ TEST(Quantity, UnitCastRequiresExplicitTypeForDangerousReps) {

// Unsafe instances: small integral types.
//
// To "test" these, try replacing `.as<Rep>(...)` with `.as(...)`. Make sure it fails with a
// To "test" these, try replacing `.coerce_as(...)` with `.as(...)`. Make sure it fails with a
// readable `static_assert`.
EXPECT_THAT(feet(uint16_t{1}).as<uint16_t /* must include */>(centi(feet)),
EXPECT_THAT(feet(uint16_t{1}).coerce_as(centi(feet)),
SameTypeAndValue(centi(feet)(uint16_t{100})));
}

TEST(Quantity, CanCastToDifferentUnit) {
EXPECT_THAT(inches(6).as<int>(feet), SameTypeAndValue(feet(0)));
EXPECT_THAT(inches(6).coerce_as(feet), SameTypeAndValue(feet(0)));
EXPECT_THAT(inches(6.).as(feet), SameTypeAndValue(feet(0.5)));
}

TEST(Quantity, QuantityCastSupportsConstexprAndConst) {
constexpr auto eighteen_inches_double = inches(18.);
constexpr auto one_foot_int = eighteen_inches_double.as<int>(feet);
constexpr auto one_foot_int = eighteen_inches_double.coerce_as<int>(feet);
EXPECT_THAT(one_foot_int, SameTypeAndValue(feet(1)));
}

Expand All @@ -544,7 +586,8 @@ TEST(Quantity, QuantityCastAvoidsPreventableOverflowWhenGoingToSmallerType) {
// Make sure we don't overflow in uint64_t.
ASSERT_EQ(lots_of_nanoinches.in(nano(inches)), would_overflow_uint32);

EXPECT_THAT(lots_of_nanoinches.as<uint32_t>(inches), SameTypeAndValue(inches(uint32_t{9})));
EXPECT_THAT(lots_of_nanoinches.coerce_as<uint32_t>(inches),
SameTypeAndValue(inches(uint32_t{9})));
}

TEST(Quantity, CommonTypeMagnitudeEvenlyDividesBoth) {
Expand Down
43 changes: 40 additions & 3 deletions docs/reference/quantity.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ fail to exist in several ways.
These last two are examples of conversions that are physically meaningful, but forbidden due to the
risk of larger-than-usual errors. The library can still perform these conversions, but not via this
constructor, and it must be "forced" to do so. See [`.as<T>(unit)`](#as) for more details.
constructor, and it must be "forced" to do so. See [`.coerce_as(unit)`](#coerce) for more details.
### Constructing from `Zero`
Expand Down Expand Up @@ -227,6 +227,12 @@ are forbidden. Additionally, the `Rep` of the output is identical to the `Rep`
2. The conversion is considered "forcing", and will be permitted in spite of any overflow or
truncation risk. The semantics are similar to `static_cast<T>`.

However, note that we may change this second property in the future. The version with the template
arguments may be changed later so that it _does_ prevent lossy conversions. If you want this
"forcing" semantic, prefer to use [`.coerce_as(unit)`](#coerce), and add the explicit template
parameter only if you want to change the rep. See
[#122](https://github.com/aurora-opensource/au/issues/122) for more details.

??? example "Example: forcing a conversion from inches to feet"
`inches(24).as(feet)` is not allowed. This conversion will divide the underlying value, `24`,
by `12`. Now, it so happens that this _particular_ value _would_ produce an integer result.
Expand Down Expand Up @@ -272,6 +278,12 @@ are forbidden. Additionally, the `Rep` of the output is identical to the `Rep`
2. The conversion is considered "forcing", and will be permitted in spite of any overflow or
truncation risk. The semantics are similar to `static_cast<T>`.

However, note that we may change this second property in the future. The version with the template
arguments may be changed later so that it _does_ prevent lossy conversions. If you want this
"forcing" semantic, prefer to use [`.coerce_in(unit)`](#coerce), and add the explicit template
parameter only if you want to change the rep. See
[#122](https://github.com/aurora-opensource/au/issues/122) for more details.

??? example "Example: forcing a conversion from inches to feet"
`inches(24).in(feet)` is not allowed. This conversion will divide the underlying value, `24`,
by `12`. Now, it so happens that this _particular_ value _would_ produce an integer result.
Expand All @@ -280,13 +292,38 @@ are forbidden. Additionally, the `Rep` of the output is identical to the `Rep`
we forbid this.

`inches(24).in<int>(feet)` _is_ allowed. The "explicit rep" template parameter has "forcing"
semantics. This would produce `2`. However, note that this operation uses integer division,
which truncates: so, for example, `inches(23).in<int>(feet)` would produce `1`.
semantics (at least for now; see [#122](https://github.com/aurora-opensource/au/issues/122)).
This would produce `2`. However, note that this operation uses integer division, which
truncates: so, for example, `inches(23).in<int>(feet)` would produce `1`.

!!! tip
Prefer to **omit** the template argument if possible, because you will get more safety checks.
The risks which the no-template-argument version warns about are real.

### Forcing lossy conversions: `.coerce_as(unit)`, `.coerce_in(unit)` {#coerce}

This function performs the exact same kind of unit conversion as if the string `coerce_` were
removed. However, it will ignore any safety checks for overflow or truncation.

??? example "Example: forcing a conversion from inches to feet"
`inches(24).as(feet)` is not allowed. This conversion will divide the underlying value, `24`,
by `12`. While this particular value would produce an integer result, most other `int` values
would not. Because our result uses `int` for storage --- same as the input --- we forbid this.

`inches(24).coerce_as(feet)` _is_ allowed. The `coerce_` prefix has "forcing" semantics. This
would produce `feet(2)`. However, note that this operation uses integer division, which
truncates: so, for example, `inches(23).coerce_as(feet)` would produce `feet(1)`.

These functions also support an explicit template parameter: so, `.coerce_as<T>(unit)` and
`.coerce_in<T>(unit)`. If you supply this parameter, it will be the rep of the result.

??? example "Example: simultaneous unit and type conversion"
`inches(27.8).coerce_as<int>(feet)` will return `feet(2)`.

!!! tip
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.

## Operations

Au includes as many common operations as possible. Our goal is to avoid incentivizing users to
Expand Down
48 changes: 44 additions & 4 deletions docs/reference/quantity_point.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ https://github.com/mkdocs/mkdocs/issues/1198#issuecomment-1253896100
Note that every case in the above table is _physically_ meaningful (because the source and target
have the same dimension), but some conversions are forbidden due to the risk of larger-than-usual
errors. The library can still perform these conversions, but not via this constructor, and it must
be "forced" to do so. See [`.as<T>(unit)`](#as) for more details.
be "forced" to do so. See [`.coerce_as(unit)`](#coerce) for more details.
### Default constructor
Expand Down Expand Up @@ -200,6 +200,12 @@ are forbidden. Additionally, the `Rep` of the output is identical to the `Rep`
2. The conversion is considered "forcing", and will be permitted in spite of any overflow or
truncation risk. The semantics are similar to `static_cast<T>`.

However, note that we may change this second property in the future. The version with the template
arguments may be changed later so that it _does_ prevent lossy conversions. If you want this
"forcing" semantic, prefer to use [`.coerce_as(unit)`](#coerce), and add the explicit template
parameter only if you want to change the rep. See
[#122](https://github.com/aurora-opensource/au/issues/122) for more details.

??? example "Example: forcing a conversion from centimeters to meters"
`centi(meters_pt)(200).as(meters_pt)` is not allowed. This conversion will divide the
underlying value, `200`, by `100`. Now, it so happens that this _particular_ value _would_
Expand Down Expand Up @@ -246,6 +252,12 @@ are forbidden. Additionally, the `Rep` of the output is identical to the `Rep`
2. The conversion is considered "forcing", and will be permitted in spite of any overflow or
truncation risk. The semantics are similar to `static_cast<T>`.

However, note that we may change this second property in the future. The version with the template
arguments may be changed later so that it _does_ prevent lossy conversions. If you want this
"forcing" semantic, prefer to use [`.coerce_in(unit)`](#coerce), and add the explicit template
parameter only if you want to change the rep. See
[#122](https://github.com/aurora-opensource/au/issues/122) for more details.

??? example "Example: forcing a conversion from centimeters to meters"
`centi(meters_pt)(200).in(meters_pt)` is not allowed. This conversion will divide the
underlying value, `200`, by `100`. Now, it so happens that this _particular_ value _would_
Expand All @@ -254,14 +266,42 @@ are forbidden. Additionally, the `Rep` of the output is identical to the `Rep`
produce integer results, we forbid this.

`centi(meters_pt)(200).in<int>(meters_pt)` _is_ allowed. The "explicit rep" template parameter
has "forcing" semantics. This would produce `2`. However, note that this operation uses integer
division, which truncates: so, for example, `centi(meters_pt)(199).in<int>(meters_pt)` would
produce `1`.
has "forcing" semantics (at least for now; see
[#122](https://github.com/aurora-opensource/au/issues/122)). This would produce `2`. However,
note that this operation uses integer division, which truncates: so, for example,
`centi(meters_pt)(199).in<int>(meters_pt)` would produce `1`.

!!! tip
Prefer to **omit** the template argument if possible, because you will get more safety checks.
The risks which the no-template-argument version warns about are real.

### Forcing lossy conversions: `.coerce_as(unit)`, `.coerce_in(unit)` {#coerce}

This function performs the exact same kind of unit conversion as if the string `coerce_` were
removed. However, it will ignore any safety checks for overflow or truncation.

??? example "Example: forcing a conversion from centimeters to meters"
`centi(meters_pt)(200).in(meters_pt)` is not allowed. This conversion will divide the
underlying value, `200`, by `100`. Now, it so happens that this _particular_ value _would_
produce an integer result. However, the compiler must decide whether to permit this operation
_at compile time_, which means we don't yet know the value. Since most `int` values would _not_
produce integer results, we forbid this.

`centi(meters_pt)(200).coerce_in(meters_pt)` _is_ allowed. The `coerce_` prefix has "forcing"
semantics. This would produce `2`. However, note that this operation uses integer division,
which truncates: so, for example, `centi(meters_pt)(199).coerce_in(meters_pt)` would produce
`1`.

These functions also support an explicit template parameter: so, `.coerce_as<T>(unit)` and
`.coerce_in<T>(unit)`. If you supply this parameter, it will be the rep of the result.

??? example "Example: simultaneous unit and type conversion"
`centi(meters_pt)(271.8).coerce_as<int>(meters_pt)` will return `meters_pt(2)`.

!!! tip
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.

## Operations

Au includes as many common operations as possible. Our goal is to avoid incentivizing users to
Expand Down
Loading

0 comments on commit 18a9afe

Please sign in to comment.