diff --git a/docs/discussion/idioms/unit-slots.md b/docs/discussion/idioms/unit-slots.md index 56ca447a..96d59b66 100644 --- a/docs/discussion/idioms/unit-slots.md +++ b/docs/discussion/idioms/unit-slots.md @@ -57,6 +57,16 @@ they have two advantages that make them easier to read: 2. You can use grammatically correct names, such as `meters / squared(second)` (note: `second` is singular), rather than `Meters{} / squared(Seconds{})`. +#### Notes for `QuantityPoint` + +`QuantityPoint` doesn't use quantity makers; it uses quantity _point_ makers. For example, instead +of the quantity maker `meters`, it uses the quantity point maker `meters_pt`. + +The implications are straightforward. If you have a `QuantityPoint` instead of a `Quantity`, then +use the quantity _point_ maker instead of the _quantity_ maker. The library will enforce this +automatically: for example, you can't pass `meters` to a `QuantityPoint`'s unit slot, and you can't +pass `meters_pt` to a `Quantity`'s unit slot. + ## Examples: rounding to RPM Let's look at some examples, using this quantity variable: diff --git a/docs/reference/quantity.md b/docs/reference/quantity.md index abf8caf3..c7a98089 100644 --- a/docs/reference/quantity.md +++ b/docs/reference/quantity.md @@ -265,7 +265,7 @@ slots](../discussion/idioms/unit-slots.md) discussion for valid choices for `uni `int`. The above example used the quantity maker, `inches`. One could also use an instance of the unit - type `Inches`, writing `length.as(Inches{})`. The former is generally preferable; the latter is + type `Inches`, writing `length.in(Inches{})`. 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 diff --git a/docs/reference/quantity_point.md b/docs/reference/quantity_point.md index 8cb64553..c2662a89 100644 --- a/docs/reference/quantity_point.md +++ b/docs/reference/quantity_point.md @@ -1,31 +1,422 @@ # QuantityPoint -`QuantityPoint` is our [affine space type](http://videocortex.io/2018/Affine-Space-Types/). The -core use cases include: +`QuantityPoint` is our [affine space type](http://videocortex.io/2018/Affine-Space-Types/). Common +example use cases include temperatures and mile markers. See our [`QuantityPoint` +discussion](../discussion/concepts/quantity_point.md) for a more detailed understanding. -- **Temperatures.** Specifically, when you want to represent "what temperature is it?" rather than - "how much did the temperature change?" +`QuantityPoint` is a template, `QuantityPoint`, with two parameters. Both are typenames. -- **Mile markers.** That is to say, points along a linear path which are indexed by distances. It - wouldn't make sense to _add_ two mile markers, but you can _subtract_ them and obtain a _distance_ - (which is a [`Quantity`](./quantity.md), not a `QuantityPoint`). +- `U` is the _unit_: a type representing the [unit of measure](./unit.md). +- `R` is the _"rep"_, a term we borrow from the [`std::chrono` + library](https://en.cppreference.com/w/cpp/chrono/duration). It's the underlying raw numeric type + which holds the wrapped value. -!!! warning "TODO: this page is a stub" +## Naming `QuantityPoint` in code -We will provide a full-fledged reference for quantity points later. For now, here are the basics. +You can use both template parameters directly (for example, `QuantityPoint`). 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: -1. `QuantityPoint` stores a value of numeric type `R` (called the "Rep"), whose units are the - [unit type](./unit.md) `U`. +| Rep-named alias | Equivalent to: | +|-----------------|----------------| +| `QuantityPointD` | `QuantityPoint` | +| `QuantityPointF` | `QuantityPoint` | +| `QuantityPointI` | `QuantityPoint` | +| `QuantityPointU` | `QuantityPoint` | +| `QuantityPointI32` | `QuantityPoint` | +| `QuantityPointU32` | `QuantityPoint` | +| `QuantityPointI64` | `QuantityPoint` | +| `QuantityPointU64` | `QuantityPoint` | - - **Example:** `QuantityPoint` represents an along-path point, whose distance is - measured in `Meters`, in a `double` variable. +## Constructing `QuantityPoint` {#constructing} -2. We provide "Rep-named aliases" for better ergonomics. +There are two ways to construct a `QuantityPoint` object. For concreteness, we'll use this +skeleton to show the signatures. - - **Example:** `QuantityPointD` is an alias for `QuantityPoint`. +```cpp +template +class QuantityPoint { -3. You cannot get values into or out of `QuantityPoint` without _explicitly naming the unit, at the - callsite_. We do not have tutorials for this yet, but it works the same as `Quantity`. In the - meantime, you can read [the `QuantityPoint` unit - tests](https://github.com/aurora-opensource/au/blob/main/au/quantity_point_test.cc) to see some - practical examples. + // A) Implicit constructor from another QuantityPoint + template + QuantityPoint(QuantityPoint other); + + // B) Default constructor + QuantityPoint(); +}; +``` + +However, the _preferred_ way to construct a `QuantityPoint` is actually neither of these. It's the +_quantity point maker_, which we describe next. + +### 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`. + +### Implicit constructor from another `QuantityPoint` {#implicit-from-quantity} + +This is option `A)` from the [previous section](#constructing). + +```cpp +template +QuantityPoint(QuantityPoint other); +``` + +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. + +This constructor only exists when this unit-and-rep conversion is both meaningful and safe. It can +fail to exist in several ways. + +1. If the input quantity has a **different dimension**, then the operation is intrinsically + meaningless and we forbid it. + +2. 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` is _not an integer_, we forbid this conversion. + + c. When the origin of `OtherUnit` is _additively offset_ from the origin of `Unit` 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](./quantity.md#implicit-from-quantity)). +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. + + + +!!! example "Examples of `QuantityPoint` to `QuantityPoint` conversions" + | Source type | Target type | Implicit construction outcome | + |-------------|-------------|---------| + | `QuantityPoint` | `QuantityPoint` {: rowspan=3} | :cross_mark: **Forbidden:** `double` source not guaranteed to hold an integer value | + | `QuantityPoint, int>` | :cross_mark: **Forbidden:** point measured in $\text{mm}$ not generally an integer in $\text{m}$ | ⁠ {: style="padding:0"} | + | `QuantityPoint, int>` | :check_mark: **Permitted:** point measured in $\text{km}$ guaranteed to be integer in $\text{m}$ | ⁠ {: style="padding:0"} | + | `QuantityPoint` {: rowspan=3} | `QuantityPoint` | :cross_mark: **Forbidden:** Zero point offset from Kelvins to Celsius is $273.15\,\, \text{K}$, a non-integer number of Kelvins | + | `QuantityPoint` | :check_mark: **Permitted:** target Rep is floating point, and can represent offset of $273.15\,\, \text{K}$ | + | `QuantityPoint, int>` | :check_mark: **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 [`.as(unit)`](#as) for more details. + +### Default constructor + +This is option `B)` from the [previous section](#constructing). + +```cpp +QuantityPoint(); +``` + +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 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. + +### `.in(unit)` {#extracting-with-in} + +This function returns the underlying stored value, by value. See the [unit +slots](../discussion/idioms/unit-slots.md) discussion for valid choices for `unit`. + +??? example "Example: extracting `4.56` from `meters_pt(4.56)`" + Consider this `QuantityPoint`: + + ```cpp + auto p = meters_pt(4.56); + ``` + + 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). + +### `.data_in(unit)` + +This function returns a reference to the underlying stored value. See the [unit +slots](../discussion/idioms/unit-slots.md) discussion for valid choices for `unit`. + +??? example "Example: incrementing the underlying stored value" + ```cpp + auto temperature = celsius_pt(20); + temperature.data_in(celsius_pt)++; + ``` + + 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(unit)` {#as} + +This function produces a new representation of the input `QuantityPoint`, converted to the new unit. +See the [unit slots](../discussion/idioms/unit-slots.md) discussion for valid choices for `unit`. + +??? example "Example: converting `meters_pt(3)` to `centi(meters_pt)`" + Consider this `QuantityPoint`: + + ```cpp + auto point = meters_pt(3); + ``` + + Then `point.as(centi(meters_pt))` re-expresses this quantity in units of centimeters. + Specifically, it returns a `QuantityPoint, 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`, writing `point.as(Centi{})`. 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](#implicit-from-quantity): 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(unit)` has two differences. + +1. The output `Rep` will be `T`. +2. The conversion is considered "forcing", and will be permitted in spite of any overflow or + truncation risk. The semantics are similar to `static_cast`. + +??? example "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`. While this particular value would produce an integer result, + most other `int` values would not. Because our result uses `int` for storage --- same as the + input --- we forbid this. + + `centi(meters_pt)(200).as(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(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(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/idioms/unit-slots.md) discussion for valid choices for `unit`. + +??? example "Example: getting the value of `meters_pt(3)` in `centi(meters_pt)`" + Consider this `QuantityPoint`: + + ```cpp + auto point = meters_pt(3); + ``` + + 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`, writing `point.in(Centi{})`. 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](#implicit-from-quantity): 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(unit)` has two differences. + +1. The output type will be `T`. +2. The conversion is considered "forcing", and will be permitted in spite of any overflow or + truncation risk. The semantics are similar to `static_cast`. + +??? example "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`. While this particular value would produce an integer result, + most other `int` values would not. Because our result uses `int` --- same as the input's rep + --- we forbid this. + + `centi(meters_pt)(200).in(meters_pt)` _is_ allowed. The "explicit rep" template parameter + has "forcing" semantics. This would produce `2`. + + However, note that this operation uses integer division, which truncates: so, for example, + `centi(meters_pt)(199).in(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. + +## 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](../discussion/concepts/arithmetic.md) 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](../discussion/concepts/arithmetic.md#common-unit). + +### Comparison + +Comparison is a [common unit operation](../discussion/concepts/arithmetic.md#common-unit). If the +input `QuantityPoint` types are not identical --- same `Unit` _and_ `Rep` --- then we first convert +them to their [common type](#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](../discussion/concepts/quantity_point.md). Addition is only defined between +a `QuantityPoint` and a `Quantity` of the same dimension. + +Addition is a [common unit operation](../discussion/concepts/arithmetic.md#common-unit). If the +input `Quantity` and `QuantityPoint` types don't have the same `Unit` _and_ `Rep`, then we first +convert them to their [common types](#common-type) --- 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} + +Subtraction is a [common unit operation](../discussion/concepts/arithmetic.md#common-unit). If the +input `QuantityPoint` types are not identical --- same `Unit` _and_ `Rep` --- then we first convert +them to their [common type](#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](./quantity.md#implicit-from-quantity) 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 p` whose rep is `R`, then `rep_cast(p)` gives a `QuantityPoint`, whose underlying value is `static_cast(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 "Example: function template" + ```cpp + template + constexpr auto refine_scale(QuantityPoint p) { + return p.as(U{} / mag<10>()); + } + ``` + + This function template will match any `QuantityPoint` specialization, and nothing else. + +??? example "Example: type template" + ```cpp + // First, we need to declare the generic type template. It matches a single type, `T`. + template + struct Size; + + // Now we can declare a specialization that matches any `QuantityPoint`, by templating on its + // unit and rep. + template + struct Size> { + + // 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::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 {#common-type} + +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>` has the following properties. + +1. It exists if and only if `U1` and `U2` have the same dimension. + +2. When it exists, it is `QuantityPoint`, where `U` is the [common + point-unit](../discussion/concepts/common_unit.md#common-quantity-point) of `U1` and `U2`, and + `R` is `std::common_type_t`. + +As [required by the standard](https://en.cppreference.com/w/cpp/types/common_type), our +`std::common_type` specializations are +[SFINAE](https://en.cppreference.com/w/cpp/language/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` and `QuantityPoint` are equivalent if and only if +**both** of the following conditions hold. + +1. The units `U1` and `U2` are [point-equivalent](./unit.md#point-equivalent). + +2. The reps `R1` and `R2` are the same type. + +**Syntax:** + +- For _types_ `U1` and `U2`: + - `AreQuantityPointTypesEquivalent::value` diff --git a/docs/reference/unit.md b/docs/reference/unit.md index 2f7351ff..709e6f3c 100644 --- a/docs/reference/unit.md +++ b/docs/reference/unit.md @@ -244,7 +244,7 @@ For example, `Meters{} * Hertz{}` is not the _same unit_ as `Meters{} / Seconds{ - For _instances_ `u1` and `u2`: - `are_units_quantity_equivalent(u1, u2)` -### Are units point-equivalent? +### Are units point-equivalent? {#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