mirror of
https://github.com/mpusz/mp-units.git
synced 2025-08-05 05:04:27 +02:00
@@ -0,0 +1,294 @@
|
|||||||
|
# Quantity Arithmetics
|
||||||
|
|
||||||
|
## `quantity` is a numeric wrapper
|
||||||
|
|
||||||
|
If we think about it, the `quantity` class template is just a "smart" numeric wrapper. It exposes
|
||||||
|
properly constrained set of arithmetic operations on one or two operands.
|
||||||
|
|
||||||
|
!!! important
|
||||||
|
|
||||||
|
Every single arithmetic operator is exposed by the `quantity` class template only if
|
||||||
|
the underlying representation type provides it as well and its implementation has proper
|
||||||
|
semantics (i.e. returns a reasonable type).
|
||||||
|
|
||||||
|
For example, in the following code, `-a` will compile only if `MyInt` exposes such an operation
|
||||||
|
as well:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
quantity a = MyInt{42} * m;
|
||||||
|
quantity b = -a;
|
||||||
|
```
|
||||||
|
|
||||||
|
Assuming that:
|
||||||
|
|
||||||
|
- `q` is our quantity,
|
||||||
|
- `qq` is a quantity implicitly convertible to `q`,
|
||||||
|
- `q2` is any other quantity,
|
||||||
|
- `kind` is a [quantity of the same kind](systems_of_quantities.md#quantities-of-the-same-kind) as `q`,
|
||||||
|
- `one` is a [quantity of `dimension_one` with the unit `one`](dimensionless_quantities.md),
|
||||||
|
- `number` is a value of a type "compatible" with `q`'s representation type,
|
||||||
|
|
||||||
|
here is the list of all the supported operators:
|
||||||
|
|
||||||
|
- unary:
|
||||||
|
- `+q`
|
||||||
|
- `-q`
|
||||||
|
- `++q`
|
||||||
|
- `q++`
|
||||||
|
- `--q`
|
||||||
|
- `q--`
|
||||||
|
- compound assignment:
|
||||||
|
- `q += qq`
|
||||||
|
- `q -= qq`
|
||||||
|
- `q %= qq`
|
||||||
|
- `q *= number`
|
||||||
|
- `q *= one`
|
||||||
|
- `q /= number`
|
||||||
|
- `q /= one`
|
||||||
|
- binary:
|
||||||
|
- `q + kind`
|
||||||
|
- `q - kind`
|
||||||
|
- `q % kind`
|
||||||
|
- `q * q2`
|
||||||
|
- `q * number`
|
||||||
|
- `number * q`
|
||||||
|
- `q / q2`
|
||||||
|
- `q / number`
|
||||||
|
- `number / q`
|
||||||
|
- ordering and comparison:
|
||||||
|
- `q == kind`
|
||||||
|
- `q <=> kind`
|
||||||
|
|
||||||
|
As we can see, there are plenty of operations one can do on a value of a `quantity` type. As most
|
||||||
|
of them are obvious, in the following chapters, we will discuss only the most important or non-trivial
|
||||||
|
aspects of quantity arithmetics.
|
||||||
|
|
||||||
|
|
||||||
|
## Addition and subtraction
|
||||||
|
|
||||||
|
Quantities can easily be added or subtracted from each other:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static_assert(1 * m + 1 * m == 2 * m);
|
||||||
|
static_assert(2 * m - 1 * m == 1 * m);
|
||||||
|
static_assert(isq::height(1 * m) + isq::height(1 * m) == isq::height(2 * m));
|
||||||
|
static_assert(isq::height(2 * m) - isq::height(1 * m) == isq::height(1 * m));
|
||||||
|
```
|
||||||
|
|
||||||
|
The above uses the same types for LHS, RHS, and the result, but in general, we can add, subtract,
|
||||||
|
or compare the values of any quantity type as long as both
|
||||||
|
[quantities are of the same kind](systems_of_quantities.md#quantities-of-the-same-kind).
|
||||||
|
The result of such an operation will be the common type of the arguments:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static_assert(1 * km + 1.5 * m == 1001.5 * m);
|
||||||
|
static_assert(isq::height(1 * m) + isq::width(1 * m) == isq::length(2 * m));
|
||||||
|
static_assert(isq::height(2 * m) - isq::distance(0.5 * m) == 1.5 * m);
|
||||||
|
static_assert(isq::radius(1 * m) - 0.5 * m == isq::radius(0.5 * m));
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
Please note that for the compound assignment operators, both arguments have to either be of
|
||||||
|
the same type or the RHS has to be implicitly convertible to the LHS, as the type of
|
||||||
|
LHS is always the result of such an operation:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static_assert((1 * m += 1 * km) == 1001 * m);
|
||||||
|
static_assert((isq::height(1.5 * m) -= 1 * m) == isq::height(0.5 * m));
|
||||||
|
```
|
||||||
|
|
||||||
|
If we break those rules, the following code will not compile:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static_assert((1 * m -= 0.5 * m) == 0.5 * m); // Compile-time error(1)
|
||||||
|
static_assert((1 * km += 1 * m) == 1001 * m); // Compile-time error(2)
|
||||||
|
static_assert((isq::height(1 * m) += isq::length(1 * m)) == 2 * m); // Compile-time error(3)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Floating-point to integral representation type is [considered narrowing](value_conversions.md).
|
||||||
|
2. Conversion of quantity with integral representation type from a unit of a higher resolution to the one
|
||||||
|
with a lower resolution is [considered narrowing](value_conversions.md).
|
||||||
|
3. Conversion from a more generic quantity type to a more specific one is
|
||||||
|
[considered unsafe](simple_and_typed_quantities.md#quantity_cast-to-force-unsafe-conversions).
|
||||||
|
|
||||||
|
|
||||||
|
## Multiplication and division
|
||||||
|
|
||||||
|
Multiplying or dividing a quantity by a number does not change its quantity type or unit. However,
|
||||||
|
its representation type may change. For example:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static_assert(isq::height(3 * m) * 0.5 == isq::height(1.5 * m));
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
Unless we use a compound assignment operator, in which case truncating operations are again not allowed:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static_assert((isq::height(3 * m) *= 0.5) == isq::height(1.5 * m)); // Compile-time error(1)
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Floating-point to integral representation type is [considered narrowing](value_conversions.md).
|
||||||
|
|
||||||
|
However, suppose we multiply or divide quantities of the same or different types, or we divide a raw
|
||||||
|
number by a quantity. In that case, we most probably will end up in a quantity of yet another type:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static_assert(120 * km / (2 * h) == 60 * (km / h));
|
||||||
|
static_assert(isq::width(2 * m) * isq::length(2 * m) == isq::area(4 * m2));
|
||||||
|
static_assert(50 / isq::time(1 * s) == isq::frequency(50 * Hz));
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! note
|
||||||
|
|
||||||
|
An exception from the above rule happens when one of the arguments is
|
||||||
|
a [dimensionless quantity](dimensionless_quantities.md). If we multiply or divide by such
|
||||||
|
a quantity, the quantity type will not change. If such a quantity has a unit `one`,
|
||||||
|
also the unit of a quantity will not change:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static_assert(120 * m / (2 * one) == 60 * m);
|
||||||
|
```
|
||||||
|
|
||||||
|
An interesting special case happens when we divide the same quantity kinds or multiply a quantity
|
||||||
|
by its inverted type. In such a case, we end up with a [dimensionless quantity](dimensionless_quantities.md).
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static_assert(isq::height(4 * m) / isq::width(2 * m) == 2 * one); // (1)!
|
||||||
|
static_assert(5 * h / (120 * min) == 0 * one); // (2)!
|
||||||
|
static_assert(5. * h / (120 * min) == 2.5 * one);
|
||||||
|
```
|
||||||
|
|
||||||
|
1. The resulting quantity type of the LHS is `isq::height / isq::width`, which is a quantity of the
|
||||||
|
dimensionless kind.
|
||||||
|
2. The resulting quantity of the LHS is `0 * dimensionless[h / min]`. To be consistent with the division
|
||||||
|
of different quantity types, we do not convert quantity values to a common unit before the division.
|
||||||
|
|
||||||
|
!!! important "Beware of integral division"
|
||||||
|
|
||||||
|
The physical units library can't do any runtime branching logic for the division operator.
|
||||||
|
All logic has to be done at compile-time when the actual values are not known, and the quantity types
|
||||||
|
can't change at runtime.
|
||||||
|
|
||||||
|
If we expect `120 * km / (2 * h)` to return `60 km / h`, we have to agree with the fact that
|
||||||
|
`5 * km / (24 * h)` returns `0 km/h`. We can't do a range check at runtime to dynamically adjust scales
|
||||||
|
and types based on the values of provided function arguments.
|
||||||
|
|
||||||
|
**This is why we often prefer floating-point representation types when dealing with units.**
|
||||||
|
Some popular physical units libraries even
|
||||||
|
[forbid integer division at all](https://aurora-opensource.github.io/au/main/troubleshooting/#integer-division-forbidden).
|
||||||
|
|
||||||
|
|
||||||
|
## Modulo
|
||||||
|
|
||||||
|
Now that we know how addition, subtraction, multiplication, and division work, it is time to talk about
|
||||||
|
modulo. What would we expect to be returned from the following quantity equation?
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
auto q = 5 * h % (120 * min);
|
||||||
|
```
|
||||||
|
|
||||||
|
Most of us would probably expect to see `1 h` or `60 min` as a result. And this is where the problems start.
|
||||||
|
|
||||||
|
C++ language defines its `/` and `%` operators with the [quotient-remainder theorem](https://eel.is/c++draft/expr.mul#4):
|
||||||
|
|
||||||
|
```text
|
||||||
|
q = a / b;
|
||||||
|
r = a % b;
|
||||||
|
q * b + r == a;
|
||||||
|
```
|
||||||
|
|
||||||
|
The important property of the modulo operation is that it only works for integral representation
|
||||||
|
types (it is undefined what modulo for floating-point types means). However, as we saw in the previous
|
||||||
|
chapter, integral types are tricky because they often truncate the value.
|
||||||
|
|
||||||
|
From the quotient-remainder theorem, the result of modulo operation is `r = a - q * b`.
|
||||||
|
Let's see what we get from such a quantity equation on integral representation types:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
const quantity a = 5 * h;
|
||||||
|
const quantity b = 120 * min;
|
||||||
|
const quantity q = a / b;
|
||||||
|
const quantity r = a - q * b;
|
||||||
|
|
||||||
|
std::cout << "reminder: " << r << "\n";
|
||||||
|
```
|
||||||
|
|
||||||
|
The above code outputs:
|
||||||
|
|
||||||
|
```text
|
||||||
|
reminder: 5 h
|
||||||
|
```
|
||||||
|
|
||||||
|
And now, a tough question needs an answer. Do we really want modulo operation on physical units
|
||||||
|
to be consistent with the quotient-remainder theorem and return `5 h` for `5 * h % (120 * min)`?
|
||||||
|
|
||||||
|
This is exactly why we decided not to follow this hugely surprising path in the **mp-units** library.
|
||||||
|
The selected approach was also consistent with the feedback from the C++ experts. For example,
|
||||||
|
this is what Richard Smith said about this issue:
|
||||||
|
|
||||||
|
!!! quote "Richard Smith"
|
||||||
|
|
||||||
|
I think the quotient-remainder property is a less important motivation here than other factors
|
||||||
|
-- the constraints on `%` and `/` are quite different, so they lack the inherent connection they
|
||||||
|
have for integers. In particular, I would expect that `A / B` works for all quantities `A` and `B`,
|
||||||
|
whereas `A % B` is only meaningful when `A` and `B` have the same dimension. It seems like
|
||||||
|
a nice-to-have for the property to apply in the case where both `/` and `%` are defined,
|
||||||
|
but internal consistency of `/` across all cases seems much more important to me.
|
||||||
|
|
||||||
|
I would expect `61 min % 1 h` to be `1 min`, and `1 h % 59 min` to also be `1 min`, so my
|
||||||
|
intuition tells me that the result type of `A % B`, where `A` and `B` have the same dimension,
|
||||||
|
should have the smaller unit of `A` and `B` (and if the smaller one doesn't divide
|
||||||
|
the larger one, we should either use the `gcd / std::common_type` of the units of
|
||||||
|
`A` and `B` or perhaps just produce an error). I think any other behavior for `%` is hard to
|
||||||
|
defend.
|
||||||
|
|
||||||
|
On the other hand, for division it seems to me that the choice of unit should probably not affect
|
||||||
|
the result, and so if we want that `5 mm / 120 min = 0 mm/min`, then `5 h / 120 min == 0 hc`
|
||||||
|
(where `hc` is a dimensionless "hexaconta", or `60x`, unit). I don't like the idea of taking
|
||||||
|
SI base units into account; that seems arbitrary and like it would do the wrong thing as often
|
||||||
|
as it does the right thing, especially when the units have a multiplier that is very large or
|
||||||
|
small. We could special-case the situation of a dimensionless quantity, but that could lead to
|
||||||
|
problematic overflow pretty easily: a calculation such as `10 s * 5 GHz * 2 uW` would overflow
|
||||||
|
an `int` if it produces a dimensionless quantity for `10 s * 5 GHz`, but it could equally
|
||||||
|
produce `50 G * 2 uW = 100 kW` without any overflow, and presumably would if the terms were merely
|
||||||
|
reordered.
|
||||||
|
|
||||||
|
If people want to use integer-valued quantities, I think it's fundamental that you need
|
||||||
|
to know what the units of the result of an operation will be, and take that into account in how you
|
||||||
|
express computations; the simplest rule for heterogeneous operators like `*` or `/` seems to be that
|
||||||
|
the units of the result are determined by applying the operator to the units of the operands
|
||||||
|
-- and for homogeneous operators like `+` or `%`, it seems like the only reasonable option is
|
||||||
|
that you get the `std::common_type` of the units of the operands.
|
||||||
|
|
||||||
|
To summarize, the modulo operation on physical units has more in common with addition and
|
||||||
|
division operators than with the quotient-remainder theorem. To avoid surprising results, the
|
||||||
|
operation uses a common unit to do the calculation and provide its result:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
static_assert(5 * h / (120 * min) == 0 * one);
|
||||||
|
static_assert(5 * h % (120 * min) == 60 * min);
|
||||||
|
static_assert(61 * min % (1 * h) == 1 * min);
|
||||||
|
static_assert(1 * h % (59 * min) == 1 * min);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Other maths
|
||||||
|
|
||||||
|
This chapter scopes only on the `quantity` type's operators. However, there are many named math
|
||||||
|
functions provided in the _mp-units/math.h_ header file. Among others, we can find there
|
||||||
|
the following:
|
||||||
|
|
||||||
|
- `pow()`, `sqrt()`, and `cbrt()`,
|
||||||
|
- `exp()`,
|
||||||
|
- `abs()`,
|
||||||
|
- `epsilon()`,
|
||||||
|
- `floor()`, `ceil()`, `round()`,
|
||||||
|
- `hypot()`,
|
||||||
|
- `sin()`, `cos()`, `tan()`,
|
||||||
|
- `asin()`, `acos()`, `atan()`.
|
||||||
|
|
||||||
|
In the library, we can also find _mp-units/random.h_ header file with all the pseudo-random number
|
||||||
|
generators.
|
||||||
|
Reference in New Issue
Block a user