Au 103: Unit Conversions¶
This tutorial explains how to perform unit conversions, both implicitly and explicitly.
- Time: 30 minutes.
- Prerequisites:
- You will learn:
- How
.in(...)
, the “value retrieving” function, actually performs unit conversions. .as(...)
: the safer cousin of.in(...)
, which gives a quantity instead of a raw number.- How to “force” a physically meaningful conversion which Au thinks is dangerous.
- Which conversions work automatically, which work only when forced, and which are prevented.
- Which implicit conversions are allowed.
- How
New units for .in(...)
¶
Once we store a value in a Quantity
, we know we need to name its unit explicitly to get the value
back out — that’s Au 101.
That API is awfully suggestive, though. What happens if we pass some other unit?
Answer: it does just what it looks like it does.
We introduced .in(...)
as, essentially, “the function that gets the value back out”. That was
true, but incomplete. Now we see its true role: it’s the quantity value conversion function. The
role we saw earlier is just a special case: when we pass the same unit we used to create it, that
“conversion” is simply the identity.
Already, this opens up a new simple, self-contained use case for Au: it’s very easy to conjure up highly readable unit conversions on the fly, even if you both start and end with raw numbers. Consider this example:
// Starting with a raw numeric type:
double angle_deg = 90.0;
{ // ⚠️ Old, manual method for doing conversions:
constexpr auto RAD_PER_DEG = M_PI / 180.0;
double angle_rad = angle_deg * RAD_PER_DEG;
}
{ // ✔️ Easy, readable ad hoc conversion with Au:
double angle_rad = degrees(angle_deg).in(radians);
}
With the old method, we needed to manually craft a carefully named conversion constant. And because
this was an angular conversion, we also needed to worry about how to get a good value for \pi
(here, we chose the M_PI
macro). By contrast, the Au-based alternative gives you a readable,
clearly correct one-liner out of the box — and it doesn’t trouble the author or reader with the
details of correctly obtaining (and using) \pi.
.as(...)
: like .in(...)
, but makes a quantity¶
Using .in(...)
works very well when you want a raw number — typically, when you’re interacting
with some legacy interface. However, sometimes what you want is a quantity that’s expressed in a
specific unit. For example, you might be comparing quantities in a hot loop, and you’d rather avoid
repeated conversions. Or, you might want to print your quantity in some specific unit.
We could satisfy these use cases with .in(...)
, but it’s a little clunky:
// Not recommended! See below.
auto angle = radians(degrees(angle_deg).in(radians));
// ^^^^^^^ Raw number--^^ ^^^^^^^
// | |
// \--Repeated identifier--/
This approach wouldn’t just be repetitive; it would also create a (small!) opportunity for error, because you temporarily leave the safety of the units library before re-entering it.
Fortunately, there’s a better choice. Quantity
has another member function, .as(...)
, for
exactly this use case. You can pass it any unit you would pass to .in(...)
, but .as(...)
returns a quantity, not a raw number. Building on our earlier example:
Use .as(...)
when you want easy, inline, fine-grained control over the units in which your
quantities are expressed.
Conversion categories¶
The examples so far have been pretty straightforward. To convert from feet
to inches
, we simply
multiply the underlying value by 12. That seems pretty safe for just about any Rep1, whether
floating point or integral. However, other conversions can be more subtle.
Let’s look at a bunch of example unit conversions. We’ll show how each conversion works with both
int
and double
Rep, because the rules can differ significantly for integral and floating point
types.
Instructions
For each example: stop and think about what you would expect the library to produce in each case. When you’re ready, click over to the “Results and Discussion” tab to check your intuition.
Example: feet
to yards
Converting from feet
to yards
means dividing the underlying value by 3.
For an integral Rep, this actually yields a compiler error, because we can’t
guarantee that the result will be an integer. True, with feet(6)
, it so happens that it
would — but if we had feet(5)
, this wouldn’t be the case!
Floating point Rep is simpler. When we divide any value by 3
, we won’t exceed typical
floating point error. Because this level of uncertainty simply goes with the territory when
using floating point types, Au allows this operation with no complaint.
Example: feet
to nano(meters)
feet(6).as(nano(meters)); // Compiler error!
feet(6.0).as(nano(meters)); // nano(meters)(1'828'800'000.0)
Converting from feet
to nano(meters)
means multiplying the underlying value by
304,800,000.
Unlike the last example, this is guaranteed to produce an integer result. Yet, the
integral Rep again gives us a compiler error! This time, we’re guarding against
a different risk: overflow. It turns out that any underlying value larger than
feet(7)
would overflow in this conversion. That’s pretty scary, so we forbid this
conversion.
Of course, that’s just because int
is typically only 32 bits. Au adapts to the specific
level of overflow risk, based on both the conversion and the range of the type. For
example, this integral-type conversion would work:
Since long long
is at least 64 bits, we could handle values into the tens of billions of
feet before overflowing! (For more details on the overflow problem, and Au’s strategies for
mitigating it, read our overflow discussion.)
As for the floating point value, this is again very safe, so we allow it without complaint.
Example: feet
to kelvins
Forcing lossy conversions: .coerce_as(...)
and .coerce_in(...)
¶
Sometimes, you may want to perform a conversion even though you know it’s usually lossy. For example, maybe you know that your particular value will give an exact result (like converting 6 feet into yards). Or perhaps the truncation is desired.
Whatever the reason, you can simply add the word “coerce” before your conversion function to make it “forcing”. Consider this example.
feet(6.0).as(yards); // yards(2.0)
// Compiler error!
// feet(6).as(yards);
feet(6).coerce_as(yards); // yards(2)
These “coercing” versions work similarly to static_cast
, in that they will truncate if necessary.
For example:
You can use this to “overrule” Au when we prevent a physically meaningful conversion because we think it’s too risky.
Tip
Prefer not to use the coercing versions unless you have a good reason. If you do, consider adding a comment to explain why your specific use case is OK.
As a code reviewer, if you see a coercing version that doesn’t seem necessary or justified, ask about it!
At this point, we’ve seen several examples of conversions which Au forbids. We’ve also seen how some of them can be forced anyway. Here’s a chance to test your understanding: what will happen if you try to force that final example — the one where the dimensions differ?
Example: forcing different-dimension conversions?
As before, stop and think about what you would expect the library to produce. When you’re ready, click over to the “Results and Discussion” tab to check your intuition.
Conversion summary¶
This table gives a visual summary of how different kinds of risks impact conversions with different storage types.
Conversion | Result (int Rep):length = feet(6) |
Result (double Rep):length = feet(6.0) |
---|---|---|
length.as(inches) |
inches(72) |
inches(72.0) |
length.as(yards) |
Forbidden: not guaranteed to be integral (can be forced with coercing version) |
yards(2.0) |
length.as(nano(meters)) |
Forbidden: excessive overflow risk (can be forced with coercing version) |
nano(meters)(1'828'800'000.0) |
length.as(kelvins) |
Forbidden: meaningless | Forbidden: meaningless |
Implicit conversions¶
Au emphasizes developer experience. We strive to provide the same ergonomics which developers have
come to expect from the venerable std::chrono
library. This means that any meaningful conversion
which we consider “safe enough” (based on the above criteria), we permit implicitly. This lets
you fluently initialize a quantity parameter with any convertible expression. For example:
// API accepting a quantity parameter.
void rotate(QuantityD<Radians> angle);
// This works!
// We'll automatically convert the integral quantity of degrees to `QuantityD<Radians>`.
rotate(degrees(45));
Our conversion policy is a refinement of the policy for std::chrono::duration
. Here
is their policy (paraphrased and simplified):
- Implicit conversions are permitted if either:
- The destination is floating point;
- Or, the source type is integer, and the conversion multiplies by an integer.
And here is our refinement (the overflow safety surface):
- If an integral-Rep conversion would overflow the destination type for a source value as small
as
2'147
, we forbid the conversion.
Deeper dive: comparing overflow strategies for Au and chrono
The std::chrono
library doesn’t consider overflow in its conversion policy, because they
handle the problem in a different way. Instead of encouraging users to use duration
directly,
they provide pre-defined helper types such as std::chrono::nanoseconds
,
std::chrono::milliseconds
, etc. The Rep for each of these types is chosen to guarantee
covering at least 292 years in either direction.
This is a good and practical solution, which is effective at preventing overflow for users who stick to these helper types. The downside is that it forces users to change their underlying storage types — changing the assembly code produced — in the process of acquiring unit safety.
A key design goal of Au is to avoid forcing users to change their underyling numeric types. We want to empower people to get the same assembly they would have had without Au, just more safely. Because smaller numeric types bring this extra overflow risk (and in a way that’s often non-obvious to developers), we designed this adaptive policy which prevents the biggest risks.
(Lastly, of course we also forbid conversions between units of different dimensions. This
consideration wasn’t part of the std::chrono
library, because that library only has a single
dimension.)
Exercise: practicing conversions¶
Check out the Au 103: Unit Conversions Exercise!
Takeaways¶
-
To convert a quantity to a particular unit, pass that unit’s quantity maker to the appropriate member function,
.in(...)
or.as(...)
.- For example,
minutes(3)
is a quantity, andminutes(3).as(seconds)
producesseconds(180)
.
- For example,
-
.in(...)
gives a raw number, while.as(...)
gives a quantity.-
These names are used consistently in this way throughout the library. For example, we’ll learn in the next tutorial that
round_in(...)
produces a raw number, whileround_as(...)
produces a quantity. -
Prefer
.as(...)
when you have a choice, because it stays within the safety of the library.
-
-
Both conversion functions include safety checks. This means you can generally just use them, and rely on the library to guard against the biggest risks. Here are the details to remember about these safety checks.
-
We forbid conversions with mismatched dimensions (as with any units library).
-
We forbid conversions for integer destinations unless we’re sure we’ll always have an integer (as with the
chrono
library), and we don’t have excessive overflow risk (an Au-original feature!).
-
-
You can force a conversion that is meaningful, but considered dangerous by preceding your function name with
coerce_
.-
For example:
seconds(200).as(minutes)
won’t compile, butseconds(200).coerce_as(minutes)
givesminutes(3)
, because we forced this lossy conversion by using the word “coerce”. -
Use this sparingly, since the safety checks are there for a reason, and consider adding a comment to explain why your usage is safe.
-
You can never force a conversion to a different dimension, because this is not meaningful.
-
-
Any conversion allowed by
.as(...)
will also work as an implicit conversion.- For example, if you have an API which takes
QuantityD<Radians>
, you can passdegrees(45)
directly to it.
- For example, if you have an API which takes
-
Recall that the “Rep” is shorthand for the underlying storage type of the quantity. ↩