Files
mp-units/doc/DESIGN.md
T

716 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `mp-units` - A Units Library for C++
## Summary
`Units` is a compile-time enabled Modern C++ library that provides compile-time dimensional
analysis and unit/quantity manipulation. The basic idea and design heavily bases on
`std::chrono::duration` and extends it to work properly with many dimensions.
Here is a small example of possible operations:
```cpp
// simple numeric operations
static_assert(10km / 2 == 5km);
// unit conversions
static_assert(1h == 3600s);
static_assert(1km + 1m == 1001m);
// dimension conversions
static_assert(1km / 1s == 1000mps);
static_assert(2kmph * 2h == 4km);
static_assert(2km / 2kmph == 1h);
static_assert(1000 / 1s == 1kHz);
static_assert(10km / 5km == 2);
```
## Approach
1. Safety and performance
- strong types
- compile-time safety
- `constexpr` all the things
- as fast or even faster than when working with fundamental types
2. The best possible user experience
- compiler errors
- debugging
3. No macros in the user interface
4. Easy extensibility
5. No external dependencies
6. Possibility to be standardized as a freestanding part of the C++ Standard Library
## Overview
The library framework consists of a few concepts: quantities, units, dimensions and their exponents. From the user's
point of view the most important is a quantity.
Quantity is a concrete amount of a unit for a specified dimension with a specific representation:
```cpp
units::quantity<units::kilometre, double> d1(123);
auto d2 = 123km; // units::quantity<units::kilometre, std::int64_t>
```
There are C++ concepts provided for each such quantity type:
```cpp
template<typename T>
concept Length = QuantityOf<T, length>;
```
With that we can easily write a function template like this:
```cpp
constexpr units::Velocity auto avg_speed(units::Length auto d, units::Time auto t)
{
return d / t;
}
```
## Basic Concepts
Below UML diagram shows the most important entities in the library design and how they relate to
each other:
![UML](units_uml.png)
### `Dimensions`
`units::dimension` represents a derived dimension and is implemented as a type-list like type that
stores an ordered list of exponents of one or more base dimensions:
```cpp
template<Exponent... Es>
struct dimension : downcast_base<dimension<Es...>> {};
```
`units::Dimension` is a concept that is satisfied by a type that is empty and publicly
derived from `units::dimension` class template:
```cpp
template<typename T>
concept Dimension =
std::is_empty_v<T> &&
detail::is_dimension<downcast_base_t<T>>; // exposition only
```
#### `Exponents`
`units::Exponent` concept is satisfied if provided type is an instantiation of `units::exp` class
template:
```cpp
template<typename T>
concept Exponent =
detail::is_exp<T>; // exposition only
```
`units::exp` provides an information about a single dimension and its (possibly fractional)
exponent in a derived dimension.
```cpp
template<typename Dim, int Num, int Den = 1>
requires BaseDimension<Dim> || Dimension<Dim>
struct exp {
using dimension = Dim;
static constexpr int num = Num;
static constexpr int den = Den;
};
```
Both a base dimension and a derived dimension can be provided to `units::exp` class template.
`units::base_dimension` represents a base dimension and has assigned a unique compile-time text
describing the dimension name:
```cpp
template<typename Name, typename Symbol>
struct base_dimension {
using name = Name;
using symbol = Symbol;
};
```
`units::BaseDimension` is a concept to match all types derived from `base_dimension` instantiations:
```cpp
template<typename T>
concept BaseDimension = std::is_empty_v<T> &&
requires {
typename T::name;
typename T::symbol;
} &&
std::derived_from<T, base_dimension<typename T::name, typename T::symbol>>;
```
For example here is a list of SI base dimensions:
```cpp
struct base_dim_length : base_dimension<"length", "m"> {};
struct base_dim_mass : base_dimension<"mass", "kg"> {};
struct base_dim_time : base_dimension<"time", "s"> {};
struct base_dim_current : base_dimension<"current", "A"> {};
struct base_dim_temperature : base_dimension<"temperature", "K"> {};
struct base_dim_substance : base_dimension<"substance", "mol"> {};
struct base_dim_luminous_intensity : base_dimension<"luminous intensity", "cd"> {};
```
#### `derived_dimension`
Above design of dimensions is created with the ease of use for end users in mind. Compile-time
errors should provide as short as possible template instantiations strings that should be easy to
understand by every engineer. Also types visible in a debugger should be easy to understand.
That is why `units::dimension` type for derived dimensions always stores information about only
those base dimensions that are used to form that derived dimension.
However, such an approach have some challenges:
```cpp
constexpr Velocity auto v1 = 1_m / 1s;
constexpr Velocity auto v2 = 2 / 2s * 1m;
static_assert(std::same_as<decltype(v1), decltype(v2)>);
static_assert(v1 == v2);
```
Above code, no matter what is the order of the base dimensions in an expression forming our result,
must produce the same `Velocity` type so that both values can be easily compared. In order to achieve
that, `dimension` class templates should never be instantiated manually but through a `derived_dimension`
helper:
```cpp
template<typename Child, Exponent... Es>
struct derived_dimension;
```
`Child` class template parameter is a part of a CRTP idiom and is used to provide a downcasting facility
described later in this document.
So for example to create a `velocity` type we have to do:
```cpp
struct velocity : derived_dimension<velocity, exp<base_dim_length, 1>, exp<base_dim_time, -1>> {};
```
In order to make `derived_dimension` work as expected it has to provide unique ordering for
contained base dimensions. Beside providing ordering to base dimensions it also has to:
- aggregate two arguments of the same base dimension but different exponents
- eliminate two arguments of the same base dimension and with opposite equal exponents
`derived_dimension` is also able to form a dimension type based not only on base dimensions but
it can take other derived dimensions as well. So for some more complex dimensions user can
type either:
```cpp
struct pressure : derived_dimension<pressure, exp<base_dim_mass, 1>, exp<base_dim_length, -1>, exp<base_dim_time, -2>> {};
```
or
```cpp
struct pressure : derived_dimension<pressure, exp<force, 1>, exp<area, -1>> {};
```
In the second case `derived_dimension` will extract all derived dimensions into the list of
exponents of base dimensions. Thanks to that both cases will result with exactly the same base
class formed only from the exponents of base units.
#### `merge_dimension`
`units::merge_dimension` is a type alias that works similarly to `derived_dimension` but instead
of sorting the whole list of base dimensions from scratch it assumes that provided input `dimension`
types are already sorted as a result of `derived_dimension`. Also contrary to `derived_dimension`
it works only with exponents of bas dimensions (no derived dimensions allowed).
Typical use case for `merge_dimension` is to produce final `dimension` return type of multiplying
two different dimensions:
```cpp
template<Dimension D1, Dimension D2>
struct dimension_multiply;
template<Exponent... E1, Exponent... E2>
struct dimension_multiply<dimension<E1...>, dimension<E2...>> {
using type = downcast_traits_t<merge_dimension_t<dimension<E1...>, dimension<E2...>>>;
};
template<Dimension D1, Dimension D2>
using dimension_multiply = dimension_multiply<typename D1::base_type, typename D2::base_type>::type;
```
### `Units`
`units::unit` is a class template that expresses the unit of a specific physical dimension:
```cpp
template<Dimension D, Ratio R>
requires (R::num * R::den > 0)
struct unit : downcast_base<unit<D, R>> {
using dimension = D;
using ratio = R;
};
```
All units are created with a `derived_unit` helper:
```cpp
template<typename Child, fixed_string Symbol, typename...>
struct derived_unit;
```
For example to define the base unit of `length`:
```cpp
struct metre : derived_unit<metre, "m", length> {};
```
Again, similarly to `derived_dimension`, the first class template parameter is a CRTP idiom used
to provide downcasting facility (described below).
`derived_unit` has a few partial useful specializations:
- helper to create a base unit of a specified dimension
```cpp
template<typename Child, fixed_string Symbol, Dimension D>
struct derived_unit<Child, Symbol, D>;
```
```cpp
struct metre : derived_unit<metre, "m", length> {};
```
- helper to create other units for a specified dimension
```cpp
template<typename Child, fixed_string Symbol, Dimension D, Ratio R>
struct derived_unit<Child, Symbol, D, R>;
```
```cpp
struct yard : derived_unit<yard, "yd", length, ratio<9'144, 10'000>> {};
```
- helper to create a unit with a SI prefix
```cpp
template<typename Child, fixed_string Symbol, Unit U>
struct derived_unit<Child, Symbol, U>;
```
```cpp
struct kilometre : derived_unit<kilometre, "km", kilo<metre>> {};
```
- helper that automatically calculates ratio based on info in desired dimension and provided list
of units of base dimensions
```cpp
template<typename Child, fixed_string Symbol, Dimension D, Unit U, Unit... Us>
struct derived_unit<Child, Symbol, D, U, Us...>;
```
```cpp
struct kilometre_per_hour : derived_unit<kilometre_per_hour, "km/h", velocity, kilometre, hour> {};
```
`units::Unit` is a concept that is satisfied by a type that is empty and publicly
derived from `units::unit` class template:
```cpp
template<typename T>
concept Unit =
std::is_empty_v<T> &&
detail::is_unit<downcast_base_t<T>>; // exposition only
```
### `Quantities`
`units::quantity` is a class template that expresses the quantity/amount of a specific dimension
expressed in a specific unit of that dimension:
```cpp
template<Unit U, Scalar Rep = double>
class quantity;
```
`units::Quantity` is a concept that is satisfied by a type that is an instantiation of `units::quantity`
class template:
```cpp
template<typename T>
concept Quantity =
detail::is_quantity<T>; // exposition only
```
`units::quantity` provides the interface really similar to `std::chrono::duration`. The difference is that
it uses `double` as a default representation and has a few additional member types and functions as below:
```cpp
template<Unit U, Scalar Rep = double>
class quantity {
public:
using unit = U;
using rep = Rep;
using dimension = U::dimension;
[[nodiscard]] static constexpr quantity one() noexcept { return quantity(quantity_values<Rep>::one()); }
template<Unit U1, Scalar Rep1, Unit U2, Scalar Rep2>
requires std::same_as<typename U1::dimension, dim_invert<typename U2::dimension>>
[[nodiscard]] constexpr Scalar operator*(const quantity<U1, Rep1>& lhs,
const quantity<U2, Rep2>& rhs);
template<Unit U1, Scalar Rep1, Unit U2, Scalar Rep2>
requires (!std::same_as<typename U1::dimension, dim_invert<typename U2::dimension>>) &&
(treat_as_floating_point<decltype(lhs.count() * rhs.count())> ||
(std::ratio_multiply<typename U1::ratio, typename U2::ratio>::den == 1))
[[nodiscard]] constexpr Quantity operator*(const quantity<U1, Rep1>& lhs,
const quantity<U2, Rep2>& rhs);
template<Scalar Rep1, typename U, typename Rep2>
[[nodiscard]] constexpr Quantity operator/(const Rep1& v,
const quantity<U, Rep2>& q);
template<Unit U1, Scalar Rep1, Unit U2, Scalar Rep2>
requires std::same_as<typename U1::dimension, typename U2::dimension>
[[nodiscard]] constexpr Scalar operator/(const quantity<U1, Rep1>& lhs,
const quantity<U2, Rep2>& rhs);
template<Unit U1, Scalar Rep1, Unit U2, Scalar Rep2>
requires (!std::same_as<typename U1::dimension, typename U2::dimension>) &&
(treat_as_floating_point<decltype(lhs.count() / rhs.count())> ||
(ratio_divide<typename U1::ratio, typename U2::ratio>::den == 1))
[[nodiscard]] constexpr Quantity operator/(const quantity<U1, Rep1>& lhs,
const quantity<U2, Rep2>& rhs);
// ...
};
```
Additional functions provide the support for operations that result in a different dimension type
than those of their arguments.
Beside adding new elements a few other changes where applied compared to the `std::chrono::duration` class:
1. The `duration` is using `std::common_type_t<Rep1, Rep2>` to find a common representation
for a calculation result. Such a design was reported as problematic by numerics study group members
as sometimes we want to provide a different type in case of multiplication and different in case of
division. `std::common_type` lacks that additional information. That is why `units::quantity` uses
the resulting type of a concrete operator operation and provides it directly to `units::common_quantity_t`
type trait.
2. `operator %` is constrained with `treat_as_floating_point` type trait to limit the types to integral
representations only. Also `operator %(Rep)` takes `Rep` as a template argument to limit implicit
conversions.
#### `quantity_cast`
To explicitly force truncating conversions `quantity_cast` function is provided which is a direct
counterpart of `std::chrono::duration_cast`. As a template argument user can provide here either
a `quantity` type or only its template parameters (`Unit`, `Rep`):
```cpp
template<Quantity To, typename U, typename Rep>
requires same_dim<typename To::dimension, typename U::dimension>
[[nodiscard]] constexpr To quantity_cast(const quantity<U, Rep>& q);
template<Unit ToU, Scalar ToRep, typename U, typename Rep>
[[nodiscard]] constexpr quantity<ToU, ToRep> quantity_cast(const quantity<U, Rep>& q);
template<Unit ToU, typename U, typename Rep>
[[nodiscard]] constexpr quantity<ToU, Rep> quantity_cast(const quantity<U, Rep>& q);
template<Scalar ToRep, typename U, typename Rep>
[[nodiscard]] constexpr quantity<U, ToRep> quantity_cast(const quantity<U, Rep>& q);
```
## Strong types instead of aliases, and type downcasting facility
Most of the important design decisions in the library are dictated by the requirement of providing
the best user experience as possible.
For example with template aliases usage the following code:
```cpp
const Velocity auto t = 20s;
```
could generate a following compile time error:
```text
<path>\example\example.cpp:39:22: error: deduced initializer does not satisfy placeholder constraints
const Velocity auto t = 20s;
^~~~
In file included from <path>\example\example.cpp:23:
<path>/src/include/units/si/velocity.h:41:16: note: within 'template<class T> concept const bool units::Velocity<T> [with T = units::quantity<units::unit<units::dimension<units::exp<units::base_dim_time, 1> >, std::ratio<1> >, long long int>]'
concept Velocity = Quantity<T> && std::same_as<typename T::dimension, velocity>;
^~~~~~~~
In file included from <path>/src/include/units/bits/tools.h:25,
from <path>/src/include/units/dimension.h:25,
from <path>/src/include/units/si/base_dimensions.h:25,
from <path>/src/include/units/si/velocity.h:25,
from <path>\example\example.cpp:23:
<path>/src/include/units/bits/stdconcepts.h:33:18: note: within 'template<class T, class U> concept const bool std::same_as<T, U> [with T = units::dimension<units::exp<units::base_dim_time, 1> >; U = units::dimension<units::exp<units::base_dim_length, 1>,units::exp<units::base_dim_time, -1> >]'
concept same_as = std::is_same_v<T, U>;
^~~~
<path>/src/include/units/bits/stdconcepts.h:33:18: note: 'std::is_same_v' evaluated to false
```
Time and velocity are not that complicated dimensions and there are much more complicated dimensions
out there, but even for those dimensions
```text
[with T = units::quantity<units::unit<units::dimension<units::exp<units::base_dim_time, 1> >, std::ratio<1> >, long long int>]
```
and
```text
[with T = units::dimension<units::exp<units::base_dim_time, 1> >; U = units::dimension<units::exp<units::base_dim_length, 1>,units::exp<units::base_dim_time, -1> >]
```
starts to be really hard to analyze or debug.
That is why it was decided to provide automated downcasting capability when possible. Thanks to this feature the
same code will result with such an error:
```text
<path>\example\example.cpp:40:22: error: deduced initializer does not satisfy placeholder constraints
const Velocity t = 20s;
^~~~
In file included from <path>\example\example.cpp:23:
<path>/src/include/units/si/velocity.h:48:16: note: within 'template<class T> concept const bool units::Velocity<T> [with T = units::quantity<units::second, long long int>]'
concept Velocity = Quantity<T> && std::same_as<typename T::dimension, velocity>;
^~~~~~~~
In file included from <path>/src/include/units/bits/tools.h:25,
from <path>/src/include/units/dimension.h:25,
from <path>/src/include/units/si/base_dimensions.h:25,
from <path>/src/include/units/si/velocity.h:25,
from <path>\example\example.cpp:23:
<path>/src/include/units/bits/stdconcepts.h:33:18: note: within 'template<class T, class U> concept const bool std::same_as<T, U> [with T = units::time; U = units::velocity]'
concept same_as = std::is_same_v<T, U>;
^~~~
<path>/src/include/units/bits/stdconcepts.h:33:18: note: 'std::is_same_v' evaluated to false
```
Now
```text
[with T = units::quantity<units::second, long long int>]
```
and
```text
[with T = units::time; U = units::velocity]
```
are not arguably much easier to understand thus provide better user experience.
Downcasting facility provides a type substitution mechanism. It connects a specific primary template
class specialization with a strong type assigned to it by the user. A simplified mental model of the
facility may be represented as:
```cpp
struct metre : unit<dimension<exp<base_dim_length, 1>>, std::ratio<1, 1, 0>>;
```
In the above example `metre` is a downcasting target (child class) and a specific `unit` class
template specialization is a downcasting source (base class). The downcasting facility provides
1 to 1 tpe substitution mechanism. Only one child class can be created for a specific base class
template instantiation.
Downcasting facility is provided through 2 dedicated types, a concept, and a few helper template aliases.
```cpp
template<typename BaseType>
struct downcast_base {
using base_type = BaseType;
friend auto downcast_guide(downcast_base);
};
```
`units::downcast_base` is a class that implements CRTP idiom, marks the base of downcasting
facility with a `base_type` member type, and provides a declaration of downcasting ADL friendly
(Hidden Friend) entry point member function `downcast_guide`. An important design point is that
this function does not return any specific type in its declaration. This non-member function
is going to be defined in a child class template `downcast_helper` and will return a target
type of the downcasting operation there.
```cpp
template<typename T>
concept Downcastable =
requires {
typename T::base_type;
} &&
std::derived_from<T, downcast_base<typename T::base_type>>;
```
`units::Downcastable` is a concepts that verifies if a type implements and can be used in a downcasting
facility.
```cpp
template<typename Target, Downcastable T>
struct downcast_helper : T {
friend auto downcast_guide(typename downcast_helper::downcast_base) { return Target(); }
};
```
`units::downcast_helper` is another CRTP class template that provides the implementation of a
non-member friend function of the `downcast_base` class template which defines the target
type of a downcasting operation. It is used in the following way to define `dimension` and
`unit` types in the library:
```cpp
template<typename Child, Exponent... Es>
struct derived_dimension : downcast_helper<Child, detail::make_dimension_t<Es...>> {};
```
```cpp
template<typename Child, fixed_string Symbol, Dimension D>
struct derived_unit<Child, Symbol, D, R> : downcast_helper<Child, unit<D, ratio<1>>> {};
```
With such CRTP types the only thing the user has to do to register a new type to the downcasting
facility is to publicly derive from one of those CRTP types and provide its new child type as
the first template parameter of the CRTP type.
```cpp
struct metre : derived_unit<metre, "m", length> {};
```
Above types are used to define base and target of a downcasting operation. To perform the actual
downcasting operation a dedicated template alias is provided:
```cpp
template<Downcastable T>
using downcast_target = decltype(detail::downcast_target_impl<T>());
```
`units::downcast_target` is used to obtain the target type of the downcasting operation registered
for a given specialization in a base type.
For example to determine a downcasted type of a quantity multiply operation the following can be done:
```cpp
using dim = dimension_multiply<typename U1::dimension, typename U2::dimension>;
using common_rep = decltype(lhs.count() * rhs.count());
using ret = quantity<downcast_target<unit<dim, ratio_multiply<typename U1::ratio, typename U2::ratio>>>, common_rep>;
```
`detail::downcast_target_impl` checks if a downcasting target is registered for the specific base class.
If yes, it returns the registered type, otherwise it works like a regular identity type returning
a provided base class.
```cpp
namespace detail {
template<typename T>
concept has_downcast = requires {
downcast_guide(std::declval<downcast_base<T>>());
};
template<typename T>
constexpr auto downcast_target_impl()
{
if constexpr(has_downcast<T>)
return decltype(downcast_guide(std::declval<downcast_base<T>>()))();
else
return T();
}
}
```
Additionally there is on more simple helper alias provided that is used in the internal
library implementation:
```cpp
template<Downcastable T>
using downcast_base_t = T::base_type;
```
## Adding custom dimensions and units
In order to extend the library with custom dimensions the user has to:
1. Create a new base dimension if the predefined ones are not enough to form a new derived dimension:
```cpp
struct base_dim_digital_information : units::base_dimension<"digital information", "b"> {};
```
2. Create a new dimension type with the recipe of how to construct it from base dimensions and
register it for a downcasting facility:
```cpp
struct digital_information : units::derived_dimension<digital_information, units::exp<base_dim_digital_information, 1>> {};
```
3. Define a concept that will match a new dimension:
```cpp
template<typename T>
concept DigitalInformation = units::QuantityOf<T, digital_information>;
```
4. Define units and register them to a downcasting facility:
```cpp
struct bit : units::derived_unit<bit, "b", digital_information> {};
struct byte : units::derived_unit<byte, "B", digital_information, units::ratio<8>> {};
```
5. Provide user-defined literals for the most important units:
```cpp
inline namespace literals {
constexpr auto operator""_b(unsigned long long l) { return units::quantity<bit, std::int64_t>(l); }
constexpr auto operator""_b(long double l) { return units::quantity<bit, long double>(l); }
constexpr auto operator""_B(unsigned long long l) { return units::quantity<byte, std::int64_t>(l); }
constexpr auto operator""_B(long double l) { return units::quantity<byte, long double>(l); }
}
```
## Open questions
1. Should we ensure that dimension is always a result of `derived_dimension` and unit is a result
of `derived_unit`? How to do it?
2. What to do with `std::chrono::duration`?
3. Should we provide `seconds<int>` or stay with `quantity<second, int>`?
4. What is the best way to add support for temperatures?
Temperature absolute values not only require `std::ratio` but also should be adjusted/shifted
by some constant values (i.e. [°C] = [K] − 273.15). Relative temperatures does need an offset.
Users will most probably have problems with differentiating those two. Maybe the best solution
is to provide only `K` support in quantity and provide non-member helper conversion functions
with verbose names to convert to `°C` and `°C`?
5. Do we need a non-linear scale?
6. Should we provide integral UDLs or just leave floating point ones?
7. Should we provide support for dimensionless quantities?
Because dimensionless quantities have no associated units, they behave as normal scalars,
and allow implicit conversion to and from the underlying value type or types that are
convertible to/from that value type.
8. Should we standardize accompany tools (downcasting facility, `type_list` operations, `common_ratio`, etc)?
9. `k`, `K`, `W`, `F` UDLs conflict with gcc GNU extensions (<https://gcc.gnu.org/onlinedocs/gcc-4.3.0/gcc/Fixed_002dPoint.html>)
for floating point types.
10. `J` imaginary constants are a GCC extension
11. Do we need custom/multiple systems?