QuantityPoint¶
QuantityPoint
is our affine space type. Common
example use cases include temperatures and mile markers. See our QuantityPoint
discussion for a more detailed understanding.
QuantityPoint
is a template, QuantityPoint<U, R>
, with two parameters. Both are typenames.
U
is the unit: a type representing the unit of measure.R
is the “rep”, a term we borrow from thestd::chrono
library. It’s the underlying raw numeric type which holds the wrapped value.
Naming QuantityPoint
in code¶
You can use both template parameters directly (for example, QuantityPoint<Meters, int>
). However,
multiple template parameters can be awkward to read. It can also give the parser some trouble when
you’re forced to use macros, as in googletest. For that reason, we also provide “Rep-named
aliases”, so you can express the rep more precisely, and put the visual focus on the unit:
Rep-named alias | Equivalent to: |
---|---|
QuantityPointD<U> |
QuantityPoint<U, double> |
QuantityPointF<U> |
QuantityPoint<U, float> |
QuantityPointI<U> |
QuantityPoint<U, int> |
QuantityPointU<U> |
QuantityPoint<U, unsigned int> |
QuantityPointI32<U> |
QuantityPoint<U, int32_t> |
QuantityPointU32<U> |
QuantityPoint<U, uint32_t> |
QuantityPointI64<U> |
QuantityPoint<U, int64_t> |
QuantityPointU64<U> |
QuantityPoint<U, uint64_t> |
Constructing QuantityPoint
¶
There are three ways to construct a QuantityPoint
object.
- The preferred way, which we’ll explain below, is to use a quantity point maker.
- The other two ways are both normal C++ constructors.
Quantity point maker (preferred)¶
The preferred way to construct a QuantityPoint
of a given unit is to use the quantity point
maker for that unit. This is a callable whose name is the plural form of that unit, expressed in
“snake_case”, and suffixed with _pt
for “point”: for example, fahrenheit_pt
. When you pass
a raw numeric variable of type T
to the quantity point maker, it returns a QuantityPoint
of the
unit it represents, whose rep type is T
.
Example
fahrenheit_pt(75)
returns a QuantityPoint<Fahrenheit, int>
.
Implicit constructor from another QuantityPoint
¶
This constructor performs a unit conversion. The result will represent the same point as the input, but expressed in the units of the newly constructed object’s type. It will also convert the stored value to the rep of the constructed object, if necessary.
Here is the signature of the constructor, slightly simplified for illustration purposes. (We’ve
enclosed it in the class template definition to be clear about Unit
and Rep
in the discussion
that follows.)
template <typename Unit, typename Rep>
class QuantityPoint {
// Implicit constructor signature (for illustration purposes):
template <typename OtherUnit, typename OtherRep>
QuantityPoint(QuantityPoint<OtherUnit, OtherRep> other);
};
This constructor only exists when this unit-and-rep conversion is both meaningful and safe. It can fail to exist in several ways.
-
If the input quantity has a different dimension, then the operation is intrinsically meaningless and we forbid it.
-
If the constructed quantity’s rep (that is,
Rep
in the code snippet above) is not floating point, then we forbid any conversion that might produce a non-integer value. Examples include:a. When
OtherRep
is floating point, we forbid this conversion.b. When
UnitRatioT<OtherUnit, Unit>
is not an integer, we forbid this conversion.c. When the origin of
OtherUnit
is additively offset from the origin ofUnit
by an amount that can’t be represented as an integer in the target units,Unit
, we forbid this conversion.
Note that case c
doesn’t occur for Quantity
; it is unique to QuantityPoint
.
We also inherit the “overflow safety surface” from the Quantity
member inside of QuantityPoint
(discussed as point 3 of the Quantity
constructor docs).
This can prevent certain quantity point conversions which have excessive overflow risk.
Here are several examples to illustrate the conditions under which implicit conversions are allowed.
Examples of QuantityPoint
to QuantityPoint
conversions
Source type | Target type | Implicit construction outcome |
---|---|---|
QuantityPoint<Meters, double> |
QuantityPoint<Meters, int> |
Forbidden: double source not guaranteed to hold an integer value |
QuantityPoint<Milli<Meters>, int> |
Forbidden: point measured in \text{mm} not generally an integer in \text{m} | |
QuantityPoint<Kilo<Meters>, int> |
Permitted: point measured in \text{km} guaranteed to be integer in \text{m} | |
QuantityPoint<Celsius, int> |
QuantityPoint<Kelvins, int> |
Forbidden: Zero point offset from Kelvins to Celsius is 273.15\,\, \text{K}, a non-integer number of Kelvins |
QuantityPoint<Kelvins, double> |
Permitted: target Rep is floating point, and can represent offset of 273.15\,\, \text{K} | |
QuantityPoint<Milli<Kelvins>, int> |
Permitted: offset in target units is 273,\!150\,\, \text{mK}, which is an integer |
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 .coerce_as(unit)
for more details.
Default constructor¶
Here is the signature of the constructor, enclosed in the class template definition for context.
A default-constructed QuantityPoint
is initialized to some value, which helps avoid certain kinds
of memory safety bugs. However, the value is contractually unspecified. You can of course look
up that value by reading the source code, but we may change it in the future, and we would not
consider this to be a breaking change. The only valid operation on a default-constructed
QuantityPoint
is to assign to it later on.
Extracting the stored value¶
In order to access the raw numeric value stored inside of QuantityPoint
, you must explicitly name
the unit at the callsite. There are two functions which can do this, depending on whether you want
to access by value or by reference.
By value: .in(unit)
¶
This function returns the underlying stored value, by value. See the unit
slots discussion for valid choices for unit
.
Example: extracting 4.56
from meters_pt(4.56)
Consider this QuantityPoint<Meters, double>
:
You can retrieve the underlying value by writing either p.in(meters_pt)
(passing the
QuantityPointMaker
), or p.in(Meters{})
(passing an instance of the unit).
By reference: .data_in(unit)
¶
This function returns a reference to the underlying stored value. See the unit
slots discussion for valid choices for unit
.
Example: incrementing the underlying stored value
Since temperature
is not const
, the reference returned by .data_in()
will be mutable, and
we can treat it like any other int
lvalue. The above would result in celsius_pt(21)
.
Performing unit conversions¶
We have two methods for performing unit conversions. They have identical APIs, but their names are
different. as
returns another QuantityPoint
, but in
exits the library and returns a raw
number.
.as(unit)
, .as<T>(unit)
¶
This function produces a new representation of the input QuantityPoint
, converted to the new unit.
See the unit slots discussion for valid choices for unit
.
Example: converting meters_pt(3)
to centi(meters_pt)
Consider this QuantityPoint<Meters, int>
:
Then point.as(centi(meters_pt))
re-expresses this quantity in units of centimeters.
Specifically, it returns a QuantityPoint<Centi<Meters>, int>
, which is equal to
centi(meters_pt)(300)
.
The above example used the quantity maker, centi(meters_pt)
. One could also use an instance
of the unit type Centi<Meters>
, writing point.as(Centi<Meters>{})
. The former is generally
preferable; the latter is mainly useful in generic code where the unit type may be all you have.
Without a template argument, .as(unit)
obeys the same safety checks as for the implicit
constructors: conversions at high risk for integer overflow or truncation
are forbidden. Additionally, the Rep
of the output is identical to the Rep
of the input.
With a template argument, .as<T>(unit)
has two differences.
- The output
Rep
will beT
. - 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)
, and add the explicit template
parameter only if you want to change the rep. See
#122 for more details.
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
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).as<int>(meters_pt)
is allowed. The “explicit rep” template parameter
has “forcing” semantics. This would produce meters_pt(2)
. However, note that this operation
uses integer division, which truncates: so, for example,
centi(meters_pt)(199).as<int>(meters_pt)
would produce meters_pt(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.
.in(unit)
, .in<T>(unit)
¶
This function produces the value of the QuantityPoint
, re-expressed in the new unit. unit
can
be either a QuantityPointMaker
for the quantity’s unit, or an instance of the unit itself. See the
unit slots discussion for valid choices for unit
.
Example: getting the value of meters_pt(3)
in centi(meters_pt)
Consider this QuantityPoint<Meters, int>
:
Then point.in(centi(meters_pt))
converts this quantity to centimeters, and returns the value,
300
, as an int
.
The above example used the quantity maker, centi(meters_pt)
. One could also use an instance
of the unit type Centi<Meters>
, writing point.in(Centi<Meters>{})
. The former is generally
preferable; the latter is mainly useful in generic code where the unit type may be all you have.
Without a template argument, .in(unit)
obeys the same safety checks as for the implicit
constructors: conversions at high risk for integer overflow or truncation
are forbidden. Additionally, the Rep
of the output is identical to the Rep
of the input.
With a template argument, .in<T>(unit)
has two differences.
- The output type will be
T
. - 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)
, and add the explicit template
parameter only if you want to change the rep. See
#122 for more details.
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).in<int>(meters_pt)
is allowed. The “explicit rep” template parameter
has “forcing” semantics (at least for now; see
#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)
¶
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: 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: 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 leave the safety of the library.
Recall that for Quantity
, there are two main categories of
operation: “arbitrary unit” operations, and “common unit” operations. However, QuantityPoint
is
different. Since multiplication, division, and powers are generally meaningless, we don’t have any
“arbitrary unit” operations: every operation is a “common unit”
operation.
Comparison¶
Comparison is a common unit operation. If the
input QuantityPoint
types are not identical — same Unit
and Rep
— then we first convert
them to their common type. Then, we perform the comparison by delegating to the
comparison operator on the underlying values.
We support the following binary comparison operators:
==
!=
>
>=
<
<=
Addition¶
Addition between any two QuantityPoint
instances is not defined, because it is not meaningful —
this is intrinsic to the core design of quantity point
types. Addition is only defined between
a QuantityPoint
and a Quantity
of the same dimension.
Addition is a common unit operation. If the
input Quantity
and QuantityPoint
types don’t have the same Unit
and Rep
, then we first
convert them to their common types — that is, we use the common unit and common
rep for each. The result is a QuantityPoint
of this common unit and rep, whose value is the sum
of the input values (after conversions).
Subtraction¶
Subtraction is a common unit operation. If the
input QuantityPoint
types are not identical — same Unit
and Rep
— then we first convert
them to their common type. The result is a Quantity
— note: not
a QuantityPoint
— of this common unit and Rep, whose value is the difference of the input values
(after any common type conversions).
Shorthand addition and subtraction (+=
, -=
)¶
The input must be a Quantity
which is implicitly
convertible to the Unit
and Rep
of the target
QuantityPoint
type. These operations first perform that conversion, and then replace the target
QuantityPoint
with the result of that addition (for +=
) or subtraction (for -=
).
rep_cast
¶
rep_cast
performs a static_cast
on the underlying value of a QuantityPoint
. It is used to
change the rep.
Given any QuantityPoint<U, R> p
whose rep is R
, then rep_cast<T>(p)
gives a QuantityPoint<U,
T>
, whose underlying value is static_cast<T>(p.in(U{}))
.
Templates and traits¶
Matching: typename U, typename R
¶
To specialize a template to match any QuantityPoint
, declare two template parameters: one for the
unit, one for the rep.
Example: function template
template <typename U, typename R>
constexpr auto refine_scale(QuantityPoint<U, R> p) {
return p.as(U{} / mag<10>());
}
This function template will match any QuantityPoint
specialization, and nothing else.
Example: type template
// First, we need to declare the generic type template. It matches a single type, `T`.
template <typename T>
struct Size;
// Now we can declare a specialization that matches any `QuantityPoint`, by templating on its
// unit and rep.
template <typename U, typename R>
struct Size<QuantityPoint<U, R>> {
// Note: this example uses inline variables, a C++17 feature. It's just for illustration.
static constexpr inline std::size_t value = sizeof(R);
};
In this way, Size<T>::value
will exist only when T
is some QuantityPoint
type (unless, of
course, other specializations get defined elsewhere).
::Rep
¶
Every QuantityPoint
type has a public static alias, ::Rep
, which indicates its underlying
storage type (or, its “rep”).
Example
decltype(meters_pt(5))::Rep
is int
.
::Unit
¶
Every QuantityPoint
type has a public static alias, ::Unit
, which indicates its unit type.
Example
decltype(meters_pt(5))::Unit
is Meters
.
::unit
¶
Every QuantityPoint
type has a public static member variable, ::unit
, which is an instance of its
unit type.
Example
decltype(meters_pt(5))::unit
is Meters{}
.
std::common_type
specialization¶
For two QuantityPoint
types, one with unit U1
and rep R1
, and the other with unit U2
and rep
R2
, then std::common_type_t<QuantityPoint<U1, R1>, QuantityPoint<U2, R2>>
has the following properties.
-
It exists if and only if
U1
andU2
have the same dimension. -
When it exists, it is
QuantityPoint<U, R>
, whereU
is the common point-unit ofU1
andU2
, andR
isstd::common_type_t<R1, R2>
.
As required by the standard, our
std::common_type
specializations are
SFINAE-friendly: improper combinations will
simply not be present, rather than producing a hard error.
AreQuantityPointTypesEquivalent¶
Result: Indicates whether two QuantityPoint
types are equivalent. Equivalent types may be
freely converted to each other, and no arithmetic operations will be performed in doing so.
More precisely, QuantityPoint<U1, R1>
and QuantityPoint<U2, R2>
are equivalent if and only if
both of the following conditions hold.
-
The units
U1
andU2
are point-equivalent. -
The reps
R1
andR2
are the same type.
Syntax:
- For types
U1
andU2
:AreQuantityPointTypesEquivalent<U1, U2>::value