Unit¶
A unit is a type which represents a unit of measure. Examples include Meters
, Radians
,
Hours
, and so on.
Users can work with units as either types or instances, and can freely convert between these representations. That is to say: units are monovalue types.
Identifying unit types¶
A unit is not forced to be a specialization of some central type template, such as a hypothetical
Unit<...>
. Rather, it’s more open ended: a unit can be any type which fulfills certain
defining properties.
To be a unit, a type U
:
-
Must contain a public type alias,
U::Dim
, which refers to a valid dimension type. -
Must contain a public type alias,
U::Mag
, which refers to a valid magnitude type. -
Must be a monovalue type.
-
May contain a
static constexpr
member namedlabel
, which is a C-styleconst char[]
(not aconst char*
).1 -
May contain a
static constexpr
member functionorigin()
, which returns a quantity whose dimension type isU::Dim
.
A custom origin()
is very rarely needed. Both labels and origins will be
discussed further below.
Making unit types¶
Although every unit type needs Dim
and Mag
members, users won’t need to add them directly.
Rather, the best way to make a unit type is by combining existing unit types via supported
operations. This approach has two key advantages relative to defining your unit as
a fully manual struct
.
-
If your input types are all valid units, your output type will be too.
-
It makes the definition more readable and physically meaningful.
To give your unit type the best ergonomics, follow our how-to guide for defining new units.
Unit labels¶
Every unit has a label. Its label is a constexpr const char[]
of the appropriate size.
For a unit type U
, or instance u
, we can access the label as follows:
unit_label<U>()
unit_label(u)
Note that the u
in unit_label(u)
is a unit slot, so you
can pass anything that “acts like a unit” to it. For instance, you can say unit_label(meters)
;
you don’t need to write unit_label(Meters{})
.
This function returns a reference to the array, which again is a compile time constant.
Note especially that the type is an array ([]
). A pointer (*
) is not acceptable. This is
so that we can support sizeof(unit_label(u))
.
Using C-style char
arrays for our labels makes Au more friendly for embedded users, because it
gives them full access to the labels without forcing them to depend on <string>
or <iostream>
.
[UNLABELED_UNIT]
¶
If a unit does not have an explicit label, we will try to generate one automatically. If we’re
unable to do so, we fall back to the “default label”, which is "[UNLABELED_UNIT]"
.
This is a label just like any other: we do not attempt to “propagate the un-labeled-ness”. As
a concrete example, if Foos
is an unlabeled unit, then the label for Nano<Foos>{} / Seconds{}
would be "n[UNLABELED_UNIT] / s"
. This is to preserve as much structure as possible for end
users, so they have the best chance of recognizing the offending unit, and perhaps upgrading it.
Note
A key design goal is for every combination of meaningfully labeled units, by every supported operation, to produce a meaningfully labeled unit. Right now, the only missing operation is scaling a unit by a magnitude. We are tracking this in #85.
Unit symbols¶
Unit symbols provide a way to create Quantity
instances concisely: by simply multiplying or
dividing a raw number by the symbol.
For example, suppose we create symbols for Meters
and Seconds
:
Then we can write 3.5f * m / s
instead of (meters / second)(3.5f)
.
Creation¶
There are two ways to create an instance of a unit symbol.
-
Call
symbol_for(your_units)
.- PRO: The argument acts as a unit slot, giving maximum flexibility and composability.
- CON: Instantiating the
symbol_for
overload adds to compilation time (although only very slightly).
-
Make an instance of
SymbolFor<YourUnits>
.- PRO: This directly uses the type itself without instantiating anything else, so it should be the fastest to compile.
- CON: Since the argument is a type, it’s less flexible and more awkward to compose.
Examples of both methods
These are easier to compose, although at the cost of instantiating an extra function.
Prefixed symbols¶
To create a symbol for a prefixed unit, both of the ways mentioned above (namely, calling
symbol_for()
, and creating a SymbolFor<>
instance) will still work. However, there is also
a third way: you can use the appropriate prefix applier with an
existing symbol for the unit to be prefixed. This can be concise and readable.
Example: creating a symbol for Nano<Meters>
Assume we have a unit Meters
, which has a quantity maker meters
and a symbol m
. Here are
your three options for creating a symbol for the prefixed unit Nano<Meters>
.
Operations¶
Each operation with a SymbolFor
consists in multiplying or dividing with some other family of
types.
Raw numeric type T
¶
Multiplying or dividing SymbolFor<Unit>
with a raw numeric type T
produces a Quantity
whose rep
is T
, and whose unit is derived from Unit
.
In the following table, we will use x
to represent the value that was stored in the input of type
T
.
Operation | Resulting Type | Underlying Value | Notes |
---|---|---|---|
SymbolFor<Unit> * T |
Quantity<Unit, T> |
x |
|
SymbolFor<Unit> / T |
Quantity<Unit, T> |
T{1} / x |
Disallowed for integral T |
T * SymbolFor<Unit> |
Quantity<Unit, T> |
x |
|
T / SymbolFor<Unit> |
Quantity<UnitInverseT<Unit>, T> |
x |
Quantity<U, R>
¶
Multiplying or dividing SymbolFor<Unit>
with a Quantity<U, R>
produces a new Quantity
. It has
the same underlying value and same rep R
, but its units U
are scaled appropriately by Unit
.
In the following table, we will use x
to represent the underlying value of the input quantity —
that is, if the input quantity was q
, then x
is q.in(U{})
.
Operation | Resulting Type | Underlying Value | Notes |
---|---|---|---|
SymbolFor<Unit> * Quantity<U, R> |
Quantity<UnitProductT<Unit, U>, R> |
x |
|
SymbolFor<Unit> / Quantity<U, R> |
Quantity<UnitQuotientT<Unit, U>, R> |
R{1} / x |
Disallowed for integral R |
Quantity<U, R> * SymbolFor<Unit> |
Quantity<UnitProductT<U, Unit>, R> |
x |
|
Quantity<U, R> / SymbolFor<Unit> |
Quantity<UnitQuotientT<U, Unit>, R> |
x |
SymbolFor<OtherUnit>
¶
Symbols compose: the product or quotient of two SymbolFor
instances is a new SymbolFor
instance.
Operation | Resulting Type |
---|---|
SymbolFor<Unit> * SymbolFor<OtherUnit> |
SymbolFor<UnitProductT<Unit, OtherUnit>> |
SymbolFor<Unit> / SymbolFor<OtherUnit> |
SymbolFor<UnitQuotientT<Unit, OtherUnit>> |
Unit origins¶
The “origin” of a unit is only useful for QuantityPoint
, our affine space
type. Even then, the origin by itself is not
meaningful. Only the difference between the origins of two units is meaningful.
You would use this to implement an “offset” unit, such as Celsius
or Fahrenheit
. However, note
that both of these are already implemented in the library.
The origin defaults to ZERO
if not supplied.
Types for combined units¶
A core tenet of Au’s design philosophy is to avoid giving any units special status. Every named unit enters into a unit computation on equal footing. We will keep track of the accumulated powers of each named unit, cancelling as appropriate. The final form will follow these rules.
-
Every power of a named unit will be represented according to the representation table. That is, it will be omitted if its power is zero, and will otherwise appear as one of
Pow
,RatioPow
, or the bare unit itself. -
If only one named unit remains with nonzero power, then that named unit power (as represented in the previous rule) is the complete type.
-
If multiple named units remain with nonzero power, then their representations (according to rule 1) are combined as the elements of a variadic
UnitProduct<...>
pack.
Warning
The ordering of the bases is deterministic, but is implementation defined, and can change at any time. It is a programming error to write code that assumes any specific ordering of the units in a pack.
A few examples
We have omitted the au::
namespace in the following examples for greater clarity.
Unit expression | Resulting unit type |
---|---|
squared(Meters{}) |
Pow<Meters, 2> |
Meters{} / Seconds{} |
UnitProduct<Meters, Pow<Seconds, -1>> |
Seconds{} * Meters{} / Seconds{} |
Meters |
Operations¶
These are the operations which each unit type supports. Because a unit must be a monovalue type, it can take the form of either a type or an instance. In what follows, we’ll use this convention:
- Capital identifiers (
U
,U1
,U2
, …) refer to types. - Lowercase identifiers (
u
,u1
,u2
, …) refer to instances.
Multiplication¶
Result: The product of two units.
Syntax:
- For types
U1
andU2
:UnitProductT<U1, U2>
- For instances
u1
andu2
:u1 * u2
Division¶
Result: The quotient of two units.
Syntax:
- For types
U1
andU2
:UnitQuotientT<U1, U2>
- For instances
u1
andu2
:u1 / u2
Powers¶
Result: A unit raised to an integral power.
Syntax:
- For a type
U
, and an integral powerN
:UnitPowerT<U, N>
- For an instance
u
, and an integral powerN
:pow<N>(u)
Roots¶
Result: An integral root of a unit.
Syntax:
- For a type
U
, and an integral rootN
:UnitPowerT<U, 1, N>
(because the N^\text{th} root is equivalent to the \left(\frac{1}{N}\right)^\text{th} power)
- For an instance
u
, and an integral rootN
:root<N>(u)
Helpers for powers and roots¶
Units support all of the power helpers. So, for example, for a unit instance
u
, you can write sqrt(u)
as a more readable alternative to root<2>(u)
.
Scaling by Magnitude
¶
Result: A new unit which has been scaled by the given magnitude. More specifically, for a unit
instance u
and magnitude instance m
, this operation:
- Preserves the dimension of
u
. - Scales the magnitude of
u
by a factor ofm
. - Deletes the label of
u
. - Preserves the origin of
u
.
Syntax:
u * m
Traits¶
Because units are monovalue types, each trait has two forms: one for types, and another for instances.
Additionally, the parameters in the instance forms will usually act as unit
slots. This means you can, for example, write
unit_ratio(feet, meters)
, which can be convenient.
Warning
The only unit trait whose parameters are not unit slots is is_unit(u)
. This is because of
its name. It will return true
only if you pass a unit type: passing the unit instance
Meters{}
returns true
, but passing the quantity maker meters
returns false
.
If you want to check whether your instance is compatible with a unit
slot, use fits_in_unit_slot(u)
.
Sections describing bool
traits will be indicated with a trailing question mark, "?"
.
Is unit?¶
Result: Indicates whether the argument is a valid unit.
Warning
We don’t currently have a trait that can detect whether or not a type is a monovalue type. Thus, the current implementation only checks whether the dimension and magnitude are valid. Until we get such a trait, authors of unit types are responsible for satisfying the monovalue type requirement.
Syntax:
- For type
U
:IsUnit<U>::value
- For instance
u
:is_unit(u)
Warning
This will only return true if u
is an instance of a unit type, such as Meters{}
. It will
return false
for a quantity maker such as meters
.
This is what you want if you are trying to figure out whether the type of your instance would be
suitable as the first template parameter for Quantity
or QuantityPoint
.
If you are trying to figure out whether your instance u
is suitable for a unit
slot, call fits_in_unit_slot(u)
instead.
Fits in unit slot?¶
Result: Indicates whether the argument can be validly passed to a unit slot in an API.
This trait is instance-only: there is no reason to apply this to types, so we do not provide a type-based API.
Syntax:
- For instance
u
:fits_in_unit_slot(u)
Warning
This can return true even if u
is not an instance of a unit type. For example,
fits_in_unit_slot(meters)
returns true, even though the type of meters
is
QuantityMaker<Meters>
, and thus, not a unit.
If you want to stringently check whether u
is a unit — say, to determine whether its type
is suitable as the first template parameter of Quantity
— then call is_unit(u)
instead.
Has same dimension?¶
Result: Indicates whether two units have the same dimension.
Syntax:
- For types
U1
andU2
:HasSameDimension<U1, U2>::value
- For instances
u1
andu2
:has_same_dimension(u1, u2)
Are units quantity-equivalent?¶
Result: Indicates whether two units are quantity-equivalent. This means that they have the same dimension and same magnitude. Quantities of quantity-equivalent units may be trivially converted to each other with no conversion factor.
For example, Meters{} * Hertz{}
is not the same unit as Meters{} / Seconds{}
, but they are
quantity-equivalent.
Syntax:
- For types
U1
andU2
:AreUnitsQuantityEquivalent<U1, U2>::value
- For instances
u1
andu2
:are_units_quantity_equivalent(u1, u2)
Are units point-equivalent?¶
Result: Indicates whether two units are point-equivalent. This means that they have the same
dimension, same magnitude, and same origin. QuantityPoint
instances of point-equivalent units
may be trivially converted to each other with no conversion factor and no additive offset.
For example, while Celsius
and Kelvins
are quantity-equivalent, they are not
point-equivalent.
Syntax:
- For types
U1
andU2
:AreUnitsPointEquivalent<U1, U2>::value
- For instances
u1
andu2
:are_units_point_equivalent(u1, u2)
Is dimensionless?¶
Result: Indicates whether the argument is a dimensionless unit.
Syntax:
- For type
U
:IsDimensionless<U>::value
- For instance
u
:is_dimensionless(u)
Is unitless unit?¶
Result: Indicates whether the argument is a “unitless unit”: that is, a dimensionless unit whose magnitude is 1.
Syntax:
- For type
U
:IsUnitlessUnit<U>::value
- For instance
u
:is_unitless_unit(u)
Unit ratio¶
Result: The magnitude representing the ratio of the input units’ magnitudes.
For units with non-trivial dimension, there is no such thing as “the” magnitude of a unit: it is not physically meaningful or observable. However, the ratio of units’ magnitudes is well defined, and that is what this trait produces.
For example, the unit ratio of Feet
and Inches
is mag<12>()
, because a foot is 12 times as big
as an inch.
Syntax:
- For types
U1
andU2
:UnitRatioT<U1, U2>::value
- For instances
u1
andu2
:unit_ratio(u1, u2)
Origin displacement¶
Result: The displacement from the first unit’s origin to the second unit’s origin.
Recall that there is no such thing as “the” origin of a unit: it is not physically meaningful or observable. However, the displacement from one unit’s origin to another is well defined, and that is what this trait produces.
For example, the origin displacement from Kelvins
to Celsius
is equivalent to
273.15 \,\text{K}.
Syntax:
- For types
U1
andU2
:OriginDisplacement<U1, U2>::value()
- For instances
u1
andu2
:origin_displacement(u1, u2)
Associated unit¶
Result: The actual unit associated with a unit slot that
is associated with a Quantity
type. Here are a few examples.
round_in(meters, feet(20));
// ^^^^^^
round_in(Meters{}, feet(20));
// ^^^^^^^^
using symbols::m;
round_in(m, feet(20));
// ^
feet(6).in(inches);
// ^^^^^^
feet(6).in(Inches{});
// ^^^^^^^^
The underlined arguments are all unit slots. The kinds of things that can be passed here include
a QuantityMaker
for a unit, a constant, a unit symbol, or simply
a unit type itself.
The use case for this trait is to implement the unit slot argument for a function.
Syntax:
- For a type
U
:AssociatedUnitT<U>
- For an instance
u
:associated_unit(u)
Associated unit (for points)¶
Result: The actual unit associated with a unit slot that is associated with a quantity point type. Here are a few examples.
round_in(meters_pt, milli(meters_pt)(1200));
// ^^^^^^^^^
round_in(Meters{}, milli(meters_pt)(1200));
// ^^^^^^^^
meters_pt(6).in(centi(meters_pt));
// ^^^^^^^^^^^^^^^^
meters_pt(6).in(Centi<Meters>{});
// ^^^^^^^^^^^^^^^
The underlined arguments are unit slots for quantity points. In practice, this will be either
a QuantityPointMaker
for some unit, or a unit itself.
The use case for this trait is to implement a function or API that takes a unit slot, and is associated with quantity points.
Syntax:
- For a type
U
:AssociatedUnitForPointsT<U>
- For an instance
u
:associated_unit_for_points(u)
Common unit¶
Result: The largest unit that evenly divides its input units. (Read more about the concept of common units.)
A specialization will only exist if all input types are units.
If the inputs are units, but their Dimensions aren’t all identical, then the request is ill-formed and we will produce a hard error.
It may happen that the input units have the same Dimension, but there is no unit which evenly divides them (because some pair of input units has an irrational quotient). In this case, there is no uniquely defined answer, but the program should still produce some answer. We guarantee that the result is associative, and symmetric under any reordering of the input units. The specific implementation choice will be driven by convenience and simplicity.
A note on inputs vs. outputs for the common_unit(us...)
form
The return value of the instance version is a unit, while the input parameters are unit slots. This means that the return value will often be a different category of thing (i.e., always consistently a unit) than the inputs (which may be quantity makers, unit symbols, or so on).
For example, consider common_unit(meters, feet)
. Recall that the type of meters
is
QuantityMaker<Meters>
, and that of feet
is QuantityMaker<Feet>
. In this case, the return
value is an instance of CommonUnitT<Meters, Feet>
, not
QuantityMaker<CommonUnitT<Meters, Feet>>
.
If you want something that still computes the common unit, but preserves the category of the
inputs, see make_common(us...)
.
Syntax:
- For types
Us...
:CommonUnitT<Us...>
- For instances
us...
:common_unit(us...)
Common point unit¶
Result: The largest-magnitude, highest-origin unit which is “common” to the units of
a collection of QuantityPoint
instances. (Read more about the concept of
common units for QuantityPoint
.)
The key goal to keep in mind is that for a QuantityPoint
of any unit U
in Us...
, converting
its value to the common point-unit should involve only:
- multiplication by a positive integer
- addition of a non-negative integer
This helps us support the widest range of Rep types (in particular, unsigned integers).
As with CommonUnitT
, this isn’t always possible: in particular, we can’t do this for units with
irrational relative magnitudes or origin displacements. However, we still provide some answer,
which is consistent with the above policy whenever it’s achievable, and produces reasonable results
in all other cases.
A specialization will only exist if the inputs are all units, and will exist but produce a hard error if any two input units have different Dimensions. We also strive to keep the result associative, and symmetric under interchange of any inputs.
A note on inputs vs. outputs for the common_point_unit(us...)
form
The return value of the instance version is a unit, while the input parameters are unit slots. This means that the return value will often be a different category of thing (i.e., always consistently a unit) than the inputs (which may be quantity point makers, unit symbols, or so on).
For example, consider common_unit(meters, feet)
. Recall that the type of meters
is
QuantityMaker<Meters>
, and that of feet
is QuantityMaker<Feet>
. In this case, the return
value is an instance of CommonUnitT<Meters, Feet>
, not
QuantityMaker<CommonUnitT<Meters, Feet>>
.
If you want something that still computes the common unit, but preserves the category of the
inputs, see make_common_point(us...)
.
Syntax:
- For types
Us...
:CommonPointUnitT<Us...>
- For instances
us...
:common_point_unit(us...)
Category-preserving unit slot operations¶
A unit slot API can take a variety of “categories” of input. Prominent examples include:
- Simple unit types (
Meters{}
, …) - Quantity makers (
meters
, …) - Unit symbols (
symbols::m
, …) - Constants (
SPEED_OF_LIGHT
, …)
The previous section demonstrated various traits that can be applied to units. Some of
these traits (such as common_unit(...)
) produce a new unit as their output type. This will always
be a simple unit, even though the inputs are unit slots: that is, these traits change the
category of the output.
This section describes a few special operations that preserve that category. So for example,
suppose we had an operation op
of this type. Let’s call the result of op(Meters{}, Seconds{})
as U{}
. Then we have:
op(meters, seconds)
producesQuantityMaker<U>
, because its inputs areQuantityMaker<Meters>
andQuantityMaker<Seconds>
.op(m, s)
producesUnitSymbol<U>
, because its inputs areUnitSymbol<Meters>
andUnitSymbol<Seconds>
.- … and so on.
Here are the category-preserving operations we provide.
Making common units¶
Result: A new unit: the largest unit that evenly divides its input units. (Read more about the concept of common units.)
Syntax:
make_common(us...)
Examples
// `meters` and `feet` are quantity makers: pass them a number, they make a quantity.
//
// `make_common(meters, feet)` is also a quantity maker, so we can pass it `18`.
constexpr auto x = make_common(meters, feet)(18);
// `m` and `ft` are unit symbols: multiply a number by them to make a quantity.
//
// `make_common(meters, feet)` is also a unit symbol, so we can multiply `9.5f` by it.
using symbols::m;
using symbols::ft;
constexpr auto y = 9.5f * make_common(m, ft);
Making common point units¶
Result: A new unit, which is “common for points” (see background info) with respect to all input units. This means that its magnitude will be the largest-magnitude unit which evenly divides both the input units and the units for any differences-of-origin. And its origin will be the lowest of all input origins.
Syntax:
make_common_point(us...)
Example
-
Unit types defined by the library may also use
au::detail::StringConstant<N>
for some integer lengthN
. Since this is in thedetail
namespace, we wanted to de-emphasize it in this document. ↩