Common Units¶
Some operations (multiplication, division, powers) work natively with arbitrary units. Others (addition, subtraction, comparison) require converting to a “common” unit. This page explains the concept, the requirements, and how we implement it.
Concepts¶
In this section, we’ll use square bracket notation, [x], to refer to a unit. Units can be multiplied by magnitudes (i.e., positive real numbers) to form new units: so, [12x] is a unit which is 12 times the size of the unit [x].
A quantity is some property which can be measured. A measurement result has two parts: the unit of measure, and the value of the quantity, which is the ratio of the quantity to that unit. For example: suppose we had some physical length, and we found that if we placed four yardsticks end-to-end, they would exactly coincide with this length. Then the measured quantity would be 4 [\text{yd}]: 4 is the value, and [\text{yd}] is the unit.
We should be careful not to say that the quantity “is” the unit/value pair! We can convert a quantity to any other unit of the same dimension by “trading off” numeric factors between the value and the unit. For example, using the fact that a yard is three feet, we can convert the above quantity to this new unit like so:
Notice that in going from line 2 to 3, we pulled the factor of 3 out of the unit, and applied it to the value. This changes the unit, and the value, but the overall quantity is unchanged. This is the key point: one quantity; many representations.
The need for common units¶
Physically, we can compare any two quantities of the same dimension. It doesn’t matter if one is
measured in feet, and the other in yards; we can place the physical lengths next to each other, and
see which one is longer. Computationally, we need to express them in the same unit, so that our
notion of <
for quantities can simply “inherit” from our notion of <
for their values.
Tip
This is exactly analogous to the need for common denominators when working with fractions. Each fraction can be expressed in many different denominators, and all of those representations represent the same number, the same element of the set \mathbb{Q}.
However, before we can add, subtract, or compare different fractions, we need to express them in the same, common denominator (analogous to units). Once we do, we can simply apply these operations directly to the numerators (analogous to values).
In principle, any unit of the same dimension can serve as the “common unit”. However, just as we tend to prefer the lowest common denominator for fractions, there is also a preferred common unit for quantities. The usual choice is the largest (i.e., greatest magnitude) unit which evenly divides both input units. This has some very nice properties.
-
Since it evenly divides both units, each conversion will end up simply multiplying by an integer (as in our example above). This lets us stay in the integer domain if we started out there.
-
Since it’s the largest such unit, we’ll be multiplying by the smallest integers that still get the job done. Not only are smaller numbers easier to work with, but when we move to the programming domain, they also reduce the risk of overflow.
Now, this isn’t always possible: for example, no unit evenly divides both degrees and radians! In those cases, our choice matters less, and it can be driven by convenience.
C++ considerations (Quantity
)¶
Note
This section only applies to Quantity
types. We follow a similar strategy for
QuantityPoint
, but with a few differences we’ll explain at the end.
The “common unit” is the unit of the common type of two or more Quantity
instances, in the sense
of std::common_type
. What properties
should it have?
Requirements¶
-
Symmetry. The common unit of any collection of input units must be independent of their ordering.
-
This flows directly from the requirements for specializing
std::common_type
, which state:Additionally,
std::common_type<T1, T2>::type
andstd::common_type<T2, T1>::type
must denote the same type.
-
-
Deduplication. Any given input unit can appear at most once in the resulting unit type.
- This is to keep compiler errors as concise and readable as possible.
- Flattening. If an input unit is a
CommonUnit<...>
type, “unpack” it and replace it with its constituent Units.- To see why, let
c(...)
be “the common unit”, andx
,y
, andz
be units. We wouldn’t wantc(x, c(y, z))
to be different fromc(x, y, z)
!
- To see why, let
- Semantic. Prefer user-meaningful units, because they show up in compiler errors. Thus, if
any input unit is equivalent to the “common unit”, we’ll prefer that input unit.
- The common unit of
Inches
andFeet
is justInches
, notCommonUnit<Inches, Feet>
!
- The common unit of
User-facing types¶
There are two main abstractions for common units which users might encounter.
CommonUnit<...>
. This is a template that defines new units from old ones, just likeUnitProduct<...>
orScaledUnit<...>
.- This should rarely, if ever be named in code.
- In implementations, we need to do this, for example, for defining the unit label of
a
CommonUnit<...>
, or defining its ordering relative to other units. - In end user code, this should probably never be named.
- In either case: never write
CommonUnit<...>
with specific template arguments! Only use it for matching.
- In implementations, we need to do this, for example, for defining the unit label of
a
- Remember:
CommonUnit<...>
can arise only as the result of some type computation.
- This should rarely, if ever be named in code.
CommonUnitT<...>
. This computes the common unit of the provided units.
Let’s clarify this relationship with an example. Suppose you’re writing a function based on two
arbitrary (but same-dimension) units, U1
and U2
, and you need their “common unit”.
- What you would write is
CommonUnitT<U1, U2>
, notCommonUnit<U1, U2>
.CommonUnitT<...>
says “please calculate the common unit”.CommonUnit<...>
says “this is the result of calculating the common unit”.
- What you get depends on the specific units.
- For
CommonUnitT<Inches, Meters>
, the result might beCommonUnit<Inches, Meters>
.1 This is because the greatest common divisor forInches
andMeters
is smaller than both of them. - For
CommonUnitT<Inches, Feet>
, the result would simply beInches
, becauseInches
is quantity-equivalent to this common unit (it evenly divides bothInches
andFeet
).
- For
Implementation approach details (deep in the weeds)
There are two main tools we use to implement CommonUnitT
.
-
FlatDedupedTypeList
. For a given variadic packList<...>
(which, for us, will beCommonUnit<...>
),FlatDedupedTypeList<List, Ts...>
will produce aList<...>
, whose elements areTs...
, but sorted according toInOrderFor<List, ...>
, and with duplicates removed.- If any of the
Ts
are alreadyList<Us...>
, we effectively “unpack” it, replacing it withUs...
. This is the “flat” part inFlatDedupedTypeList
.
- If any of the
-
FirstQuantityEquivalentUnit
. The above step produces aCommonUnit<...>
specialization, which itself meets the definition of a unit. But is it the unit we really want to provide? Not if there’s a simpler one!FirstQuantityEquivalentUnit<CommonUnit<Us...>>
searches through the unit listUs...
, and returns the first quantity-equivalent one it finds. If no such unit is available, then we fall back to returningCommonUnit<Us...>
.
Changes for QuantityPoint
¶
The common unit for QuantityPoint
operations is different from the common
unit for Quantity
. To see why a single notion of “common unit” isn’t enough, consider Celsius
and Kelvins
.
-
For a
Quantity
, these two units are identical. The “common unit” will be (quantity-)equivalent to both of them. -
For a
QuantityPoint
, these units are very different. A “temperature point” of 0 degreesCelsius
is (point-)equivalent to a temperature point of 273.15Kelvins
. This additive offset means that we’ll need to convert both toCenti<Kelvins>
before we can subtract and/or compare them!
Thus, what we’ve been calling CommonUnitT
is really more like CommonQuantityUnitT
(although
we’ve kept the name short because Quantity
is by far the typical use case). For QuantityPoint
operations, we have the CommonPointUnitT<Us...>
alias, which typically creates some instance of
CommonPointUnit<Us...>
with the Us...
in their canonical ordering.
So: what is the “common quantity point unit”? Well, we can start with the “common quantity unit,” but the origin adds a new complication. We’ll need to choose a convention.
-
With “common quantity units,” our convention ensured that conversions could only multiply by a positive integer. This keeps us within the domain of the integers whenever we start there. And we chose the smallest such number to minimize overflow risk.
-
Similarly, with “common quantity point units,” we should choose its origin such that we only add a non-negative integer. This convention preserves and extends the previous one: not only are we keeping integers as integers, but we support unsigned integers as best we can.
-
It might also be
CommonUnit<Meters, Inches>
. The ordering is deterministic, but unspecified. ↩