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)
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 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¶
Note
We plan to make these available for Magnitude
and
Dimension
as well. See
#84 to track progress.
Each of the following helpers are available to operate on a unit instance, u
:
Helper | Result |
---|---|
inverse(u) |
pow<-1>(u) |
squared(u) |
pow<2>(u) |
cubed(u) |
pow<3>(u) |
sqrt(u) |
root<2>(u) |
cbrt(u) |
root<3>(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¶
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)
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¶
TODO: this is a stub.
Common unit¶
TODO: this is a stub.
Common point unit¶
TODO: this is a stub.
-
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. ↩