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.
Uis the unit: a type representing the unit of measure.Ris the “rep”, a term we borrow from thestd::chronolibrary. 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,
Repin the code snippet above) is not floating point, then we forbid any conversion that might produce a non-integer value. Examples include:a. When
OtherRepis floating point, we forbid this conversion.b. When
UnitRatioT<OtherUnit, Unit>is not an integer, we forbid this conversion.c. When the origin of
OtherUnitis additively offset from the origin ofUnitby 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> |
double source not guaranteed to hold an integer value |
QuantityPoint<Milli<Meters>, int> |
| |
QuantityPoint<Kilo<Meters>, int> |
| |
QuantityPoint<Celsius, int> |
QuantityPoint<Kelvins, int> |
|
QuantityPoint<Kelvins, double> |
||
QuantityPoint<Milli<Kelvins>, int> |
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
Repwill 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
U1andU2have the same dimension. -
When it exists, it is
QuantityPoint<U, R>, whereUis the common point-unit ofU1andU2, andRisstd::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
U1andU2are point-equivalent. -
The reps
R1andR2are the same type.
Syntax:
- For types
U1andU2:AreQuantityPointTypesEquivalent<U1, U2>::value