feat: 💥 delta and absolute construction helpers

This commit is contained in:
Mateusz Pusz
2024-06-30 12:03:24 +02:00
parent ad3035eafe
commit ba8681f90b
24 changed files with 309 additions and 330 deletions

View File

@@ -37,7 +37,7 @@ std::cout << Pressure << "\n";
The problem is related to the accidental usage of a `quantity` rather than `quantity_point` for
`Temperature`. This means that after conversion to kelvins, we will get `28 K` instead of
the expected `301.15 K`, which will corrupt all further calculations.
the expected `301.15 K`, corrupting all further calculations.
A correct code should use a `quantity_point`:
@@ -48,51 +48,68 @@ quantity_point Temperature(28.0 * deg_C);
This might be an obvious thing for domain experts, but new users of the library may not be aware
of the affine space abstractions and how they influence temperature handling.
After a lengthy discussion on handling such scenarios, we decided to aid the `quantity` and
`quantity_point` construction with `absolute<Reference>` and `delta<Reference>` quantity reference
specifiers. This applies to:
After a lengthy discussion on handling such scenarios, we decided to:
- the multiply syntax,
- 2-parameter `quantity` constructor.
- make the above code ill-formed,
- provide an alternative way to create a `quantity` with the `delta` quantity construction helper.
Here are the main points of this new design:
1. All references/units that do not specify point origin (are not offset units) in their definition
are considered `delta` by default. This means that `42 * m` creates a `quantity` and is
the same as calling `42 * delta<m>`.
2. Multiply syntax is extended to allow `quantity_point` creation with the `42 * absolute<m>`
syntax. This will provide an implicit zeroth point origin.
3. For units that specify a point origin (`si::kelvin`, `si::degree_Celsius`, and
   `usc::degree_Fahrenheit`), the user always needs to specify a modifier. This means that:
- `4 * deg_C` does not compile,
- `4 * delta<deg_C>` creates a `quantity`.
- `4 * absolute<deg_C>` creates a `quantity_point`.
4. The 2-parameter `quantity` constructor requires the same:
1. All references/units that specify point origin in their definition (i.e., `si::kelvin`,
   `si::degree_Celsius`, and `usc::degree_Fahrenheit`) are excluded from the multiply syntax
(:boom: **breaking change** :boom:).
2. A new `delta` quantity construction helper is introduced:
```cpp
quantity q1(4, m); // OK
quantity q2(4, delta<m>); // OK
quantity q3(4, absolute<m>); // Compile-time error
quantity q4(4, deg_C); // Compile-time error
quantity q5(4, delta<deg_C>); // OK
quantity q6(4, absolute<deg_C>); // Compile-time error
```
- `delta<m>(42)` results with a `quantity<si::metre, int>`,
- `delta<deg_C>(5)` results with a `quantity<si::deg_C, int>`.
The `delta` and `absolute` modifiers are stripped upon construction, so the resulting `quantity`
and `quantity_point` types use the underlying unit in its type.
3. A new `absolute` quantity point construction helper is introduced:
With such changes, the offending code will not compile, forcing the user to think more about what
is written. To enable the compilation, the user has to type one of the following:
- `absolute<m>(42)` results with a `quantity_point<si::metre, zeroth_point_origin<kind_of<isq::length>>{}, int>`,
- `absolute<deg_C>(5)` results with a `quantity<si::metre, si::ice_point, int>`.
- `quantity_point Temperature(28.0 * delta<deg_C>);`
- `quantity_point Temperature = 28.0 * absolute<deg_C>;`
!!! info
If the user still insists on using `quantity` instead of a `quantity_point`, the code will
have to be written in the following way to compile successfully:
Please note that `si::kelvin` is also excluded from the multiply syntax to prevent the
following surprising issues:
```cpp
quantity Temperature = 28.0 * delta<deg_C>;
```
=== "Now"
This will yield an invalid result, but now, hopefully, it is clearly readable in the code what is
wrong here.
```cpp
quantity q = delta<K>(300);
quantity_point qp = absolute<K>(300);
static_assert(q.in(deg_C) != qp.in(deg_C).quantity_from_zero());
```
=== "Before"
```cpp
quantity q(300 * K);
quantity_point qp(300 * K);
static_assert(q.in(deg_C) != qp.in(deg_C).quantity_from_zero());
```
We believe that the code enforced with new utilities makes it much easier to understand what
happens here.
With such changes to the interface design, the offending code will not compile as initially written.
Users will be forced to think more about what they write. To enable the compilation, the users have
to explicitly create a:
- `quantity_point` (the intended abstraction in this example) with any of the below syntaxes:
```cpp
quantity_point Temperature = absolute<deg_C>(28.0);
auto Temperature = absolute<deg_C>(28.0);
quantity_point Temperature(delta<deg_C>(28.0));
```
- `quantity` (an incorrect abstraction in this example) with:
```cpp
quantity Temperature = delta<deg_C>(28.0);
auto Temperature = delta<deg_C>(28.0);
```
Thanks to the new design, we can immediately see what happens here and why the result might be
incorrect in the second case.

View File

@@ -43,28 +43,51 @@ a number with a predefined unit:
!!! info
In case someone doesn't like the multiply syntax or there is an ambiguity between `operator*`
provided by this and other libraries, a quantity can also be created with a two-parameter
constructor:
provided by this and other libraries, there are two other ways to create a quantity:
=== "C++ modules"
1. `delta` construction helper:
```cpp
import mp_units;
=== "C++ modules"
using namespace mp_units;
```cpp
import mp_units;
quantity q{42, si::metre / si::second};
```
using namespace mp_units;
=== "Header files"
quantity q = delta<si::metre / si::second>(42);
```
```cpp
#include <mp-units/systems/si.h>
=== "Header files"
using namespace mp_units;
```cpp
#include <mp-units/systems/si.h>
quantity q{42, si::metre / si::second};
```
using namespace mp_units;
quantity q = delta<si::metre / si::second>(42);
```
2. A two-parameter constructor:
=== "C++ modules"
```cpp
import mp_units;
using namespace mp_units;
quantity q{42, si::metre / si::second};
```
=== "Header files"
```cpp
#include <mp-units/systems/si.h>
using namespace mp_units;
quantity q{42, si::metre / si::second};
```
The above creates an instance of `quantity<derived_unit<si::metre, per<si::second>>{}, int>`.
The same can be obtained using optional unit symbols:
@@ -238,7 +261,7 @@ This introduces an additional type-safety.
using namespace mp_units::si::unit_symbols;
using namespace mp_units::usc::unit_symbols;
quantity_point temp = 20. * absolute<deg_C>;
quantity_point temp = absolute<deg_C>(20.);
std::println("Temperature: {} ({})",
temp.quantity_from_zero(),
temp.in(deg_F).quantity_from_zero());
@@ -259,7 +282,7 @@ This introduces an additional type-safety.
using namespace mp_units::si::unit_symbols;
using namespace mp_units::usc::unit_symbols;
quantity_point temp = 20. * absolute<deg_C>;
quantity_point temp = absolute<deg_C>(20.);
std::println("Temperature: {} ({})",
temp.quantity_from_zero(),
temp.in(deg_F).quantity_from_zero());

View File

@@ -378,5 +378,5 @@ For example:
the previous example:
```cpp
constexpr auto room_reference_temperature = ice_point + isq::Celsius_temperature(21 * delta<deg_C>);
constexpr auto room_reference_temperature = ice_point + delta<isq::Celsius_temperature[deg_C]>(21);
```

View File

@@ -68,51 +68,17 @@ difference between two things:
- the difference in _speed_ (even if relative to zero).
As we already know, a `quantity` type provides all operations required for a _displacement vector_
abstraction in an affine space.
abstraction in the affine space. It can be constructed with:
Quantities are constructed from a delta quantity reference. Most of units are considered to be
delta references by default. The ones that need a special qualification are the units that
get a point origin in their definition (i.e., units of temperature).
We can create a `quantity` by passing a delta quantity reference to either:
- two-parameter constructor:
```cpp
quantity q1(42, si::metre);
// quantity q2(42, si::kelvin); // Compile-time error
// quantity q3(42, si::degree_Celsius); // Compile-time error
// quantity q4(42, usc::degree_Fahrenheit); // Compile-time error
quantity q5(42, delta<si::metre>);
quantity q6(42, delta<si::kelvin>);
quantity q7(42, delta<si::degree_Celsius>);
quantity q8(42, delta<usc::degree_Fahrenheit>);
```
- multiply syntax:
```cpp
quantity q1 = 42 * m;
// quantity q2 = 42 * K; // Compile-time error
// quantity q3 = 42 * deg_C; // Compile-time error
// quantity q4 = 42 * deg_F; // Compile-time error
quantity q5 = 42 * delta<m>;
quantity q6 = 42 * delta<K>;
quantity q7 = 42 * delta<deg_C>;
quantity q8 = 42 * delta<deg_F>;
```
- the multiply syntax (works for most of the units),
- `delta<Reference>` construction helper (e.g., `delta<isq::height[m]>(42)`, `delta<deg_C>(3)`),
- two-parameter constructor taking a number and a quantity reference/unit.
!!! note
`delta` specifier is used to qualify the entire reference upon `quantity` construction.
It does not satisfy the [`Reference`](concepts.md#Reference) concept. This means that,
for example, the below are ill-formed:
The multiply syntax support is disabled for units that provide a point origin in their
definition (i.e., units of temperature like `K`, `deg_C`, and `deg_F`).
```cpp
void foo(quantity<delta<si::degree_Celsius>> temp); // ill-formed
quantity<N * m / (delta<deg_C> * mol)> specific_heat_capacity; // ill-formed
quantity R = 8.314 * N * m / (delta<deg_C> * mol); // ill-formed
```
## _Point_ is modeled by `quantity_point` and `PointOrigin`
@@ -152,17 +118,19 @@ scale zeroth point using the following rules:
- otherwise, an instantiation of `zeroth_point_origin<QuantitySpec>` is being used which
provides a well-established zeroth point for a specific quantity type.
Quantity points with default point origins may be constructed using multiply syntax from an
absolute quantity reference. None of units are considered to be absolute references by default,
so they need a special qualification:
Quantity points with default point origins may be constructed with the `absolute` construction
helper or forcing an explicit conversion from the `quantity`:
```cpp
// quantity_point qp1 = 42 * m; // Compile-time error
// quantity_point qp2 = 42 * K; // Compile-time error
// quantity_point qp3 = 42 * deg_C; // Compile-time error
quantity_point qp4 = 42 * absolute<m>;
quantity_point qp5 = 42 * absolute<K>;
quantity_point qp6 = 42 * absolute<deg_C>;
// quantity_point qp1 = 42 * m; // Compile-time error
// quantity_point qp2 = 42 * K; // Compile-time error
// quantity_point qp3 = delta<deg_C>(42); // Compile-time error
quantity_point qp4(42 * m);
quantity_point qp5(42 * K);
quantity_point qp6(delta<deg_C>(42));
quantity_point qp7 = absolute<m>(42);
quantity_point qp8 = absolute<K>(42);
quantity_point qp9 = absolute<deg_C>(42);
```
!!! tip
@@ -180,8 +148,8 @@ for this domain.
![affine_space_1](affine_space_1.svg){style="width:80%;display: block;margin: 0 auto;"}
```cpp
quantity_point<isq::distance[si::metre]> qp1 = 100 * absolute<m>;
quantity_point<isq::distance[si::metre]> qp2 = 120 * absolute<m>;
quantity_point<isq::distance[si::metre]> qp1(100 * m);
quantity_point<isq::distance[si::metre]> qp2 = absolute<m>(120);
assert(qp1.quantity_from_zero() == 100 * m);
assert(qp2.quantity_from_zero() == 120 * m);
@@ -210,7 +178,7 @@ compatible:
```cpp
quantity_point<si::metre> qp1{isq::distance(100 * m)};
quantity_point<si::metre> qp2{isq::height(120 * m)};
quantity_point<si::metre> qp2 = absolute<isq::height[m]>(120);
assert(qp2.quantity_from(qp1) == 20 * m);
assert(qp1.quantity_from(qp2) == -20 * m);
@@ -230,8 +198,8 @@ origin.
```cpp
inline constexpr struct origin final : absolute_point_origin<isq::distance> {} origin;
// quantity_point<si::metre, origin> qp1{100 * m}; // Compile-time error
// quantity_point<si::metre, origin> qp2 = 120 * absolute<m>; // Compile-time error
// quantity_point<si::metre, origin> qp1{100 * m}; // Compile-time error
// quantity_point<si::metre, origin> qp2{delta<m>(120)}; // Compile-time error
quantity_point<si::metre, origin> qp1 = origin + 100 * m;
quantity_point<si::metre, origin> qp2 = 120 * m + origin;
@@ -443,7 +411,7 @@ namespace si {
inline constexpr struct absolute_zero final : absolute_point_origin<isq::thermodynamic_temperature> {} absolute_zero;
inline constexpr auto zeroth_kelvin = absolute_zero;
inline constexpr struct ice_point final : relative_point_origin<273'150 * absolute<milli<kelvin>>>> {} ice_point;
inline constexpr struct ice_point final : relative_point_origin<absolute<milli<kelvin>>(273'150)}> {} ice_point;
inline constexpr auto zeroth_degree_Celsius = ice_point;
}
@@ -451,7 +419,7 @@ inline constexpr auto zeroth_degree_Celsius = ice_point;
namespace usc {
inline constexpr struct zeroth_degree_Fahrenheit final :
relative_point_origin<-32 * absolute<mag_ratio<5, 9> * si::degree_Celsius>> {} zeroth_degree_Fahrenheit;
relative_point_origin<absolute<mag_ratio<5, 9> * si::degree_Celsius>(-32)> {} zeroth_degree_Fahrenheit;
}
```
@@ -500,28 +468,28 @@ choose from here. Depending on our needs or tastes, we can:
- be explicit about the unit and origin:
```cpp
quantity_point<si::degree_Celsius, si::zeroth_degree_Celsius> q1 = si::zeroth_degree_Celsius + 20.5 * delta<deg_C>;
quantity_point<si::degree_Celsius, si::zeroth_degree_Celsius> q2 = {20.5 * delta<deg_C>, si::zeroth_degree_Celsius};
quantity_point<si::degree_Celsius, si::zeroth_degree_Celsius> q3{20.5 * delta<deg_C>};
quantity_point<si::degree_Celsius, si::zeroth_degree_Celsius> q4 = 20.5 * absolute<deg_C>;
quantity_point<si::degree_Celsius, si::zeroth_degree_Celsius> q1 = si::zeroth_degree_Celsius + delta<deg_C>(20.5);
quantity_point<si::degree_Celsius, si::zeroth_degree_Celsius> q2 = {delta<deg_C>(20.5), si::zeroth_degree_Celsius};
quantity_point<si::degree_Celsius, si::zeroth_degree_Celsius> q3{delta<deg_C>(20.5)};
quantity_point<si::degree_Celsius, si::zeroth_degree_Celsius> q4 = absolute<deg_C>(20.5);
```
- specify a unit and use its zeroth point origin implicitly:
```cpp
quantity_point<si::degree_Celsius> q5 = si::zeroth_degree_Celsius + 20.5 * delta<deg_C>;
quantity_point<si::degree_Celsius> q6 = {20.5 * delta<deg_C>, si::zeroth_degree_Celsius};
quantity_point<si::degree_Celsius> q7{20.5 * delta<deg_C>};
quantity_point<si::degree_Celsius> q8 = 20.5 * absolute<deg_C>;
quantity_point<si::degree_Celsius> q5 = si::zeroth_degree_Celsius + delta<deg_C>(20.5);
quantity_point<si::degree_Celsius> q6 = {delta<deg_C>(20.5), si::zeroth_degree_Celsius};
quantity_point<si::degree_Celsius> q7{delta<deg_C>(20.5)};
quantity_point<si::degree_Celsius> q8 = absolute<deg_C>(20.5);
```
- benefit from CTAD:
```cpp
quantity_point q9 = si::zeroth_degree_Celsius + 20.5 * delta<deg_C>;
quantity_point q10 = {20.5 * delta<deg_C>, si::zeroth_degree_Celsius};
quantity_point q11{20.5 * delta<deg_C>};
quantity_point q12 = 20.5 * absolute<deg_C>;
quantity_point q9 = si::zeroth_degree_Celsius + delta<deg_C>(20.5);
quantity_point q10 = {delta<deg_C>(20.5), si::zeroth_degree_Celsius};
quantity_point q11{delta<deg_C>(20.5)};
quantity_point q12 = absolute<deg_C>(20.5);
```
In all of the above cases, we end up with the `quantity_point` of the same type and value.
@@ -532,10 +500,10 @@ the following way:
![affine_space_6](affine_space_6.svg){style="width:80%;display: block;margin: 0 auto;"}
```cpp
constexpr struct room_reference_temp final : relative_point_origin<21 * absolute<deg_C>> {} room_reference_temp;
constexpr struct room_reference_temp final : relative_point_origin<absolute<deg_C>(21)> {} room_reference_temp;
using room_temp = quantity_point<isq::Celsius_temperature[deg_C], room_reference_temp>;
constexpr auto step_delta = isq::Celsius_temperature(0.5 * delta<deg_C>);
constexpr auto step_delta = delta<isq::Celsius_temperature<deg_C>>(0.5);
constexpr int number_of_steps = 6;
room_temp room_ref{};