Documentation updated

This commit is contained in:
Mateusz Pusz
2020-05-13 12:50:14 +02:00
parent cf7dab42c3
commit df6a0da9d3
11 changed files with 499 additions and 996 deletions

View File

@ -70,10 +70,12 @@ add_custom_command(OUTPUT "${SPHINX_INDEX_FILE}"
"${CMAKE_CURRENT_SOURCE_DIR}/CHANGELOG.md"
"${CMAKE_CURRENT_SOURCE_DIR}/design.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/design/directories.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/design/downcasting.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/design/quantity.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/examples.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/examples/hello_units.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/examples/avg_speed.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/examples/box_example.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/examples/hello_units.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/examples/linear_algebra.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/examples/measurement.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/faq.rst"
@ -100,7 +102,7 @@ add_custom_command(OUTPUT "${SPHINX_INDEX_FILE}"
"${CMAKE_CURRENT_SOURCE_DIR}/use_cases/legacy_interfaces.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/use_cases/linear_algebra.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/use_cases/natural_units.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/use_cases/unknown_units_and_dimensions.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/use_cases/unknown_dimensions.rst"
"${CMAKE_CURRENT_SOURCE_DIR}/usage.rst"
"${DOXYGEN_INDEX_FILE}"
MAIN_DEPENDENCY "${SPHINX_SOURCE}/conf.py"

View File

@ -1,965 +0,0 @@
# `mp-units` - Design Overview
## Summary
`mp-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(10q_km / 2 == 5q_km);
// unit conversions
static_assert(1q_h == 3600q_s);
static_assert(1q_km + 1q_m == 1001q_m);
// dimension conversions
static_assert(1q_km / 1q_s == 1000q_m_per_s);
static_assert(2q_km_per_h * 2q_h == 4q_km);
static_assert(2q_km / 2q_km_per_h == 1q_h);
static_assert(1000 / 1q_s == 1q_kHz);
static_assert(10q_km / 5q_km == 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
## Basic Concepts
The most important concepts in the library are `Unit`, `Dimension`, and `Quantity`:
![Design UML](_static/img/concepts.png)
`Unit` is a basic building block of the library. Every dimension works with a concrete
hierarchy of units. Such hierarchy defines a reference unit and often a few scaled versions of
it.
`Dimension` concept matches a dimension of either a base or derived quantity. `base_dimension`
is instantiated with a unique symbol identifier and a base unit. `derived_unit` is a list of
exponents of either base or other derived dimensions.
`Quantity` is a concrete amount of a unit for a specified dimension with a specific
representation.
## `Unit`
All units are represented in the framework by a `scaled_unit` class template:
```cpp
template<UnitRatio R, typename U>
struct scaled_unit : downcast_base<scaled_unit<R, U>> {
using ratio = R;
using reference = U;
};
```
where:
```cpp
template<typename R>
concept UnitRatio = Ratio<R> && R::num > 0 && R::den > 0; // double negatives not allowed
```
and `Ratio` is satisfied by any instantiation of `units::ratio<Num, Den, Exp>`.
The `scaled_unit` type is a framework's private type and the user should never instantiate it directly.
The public user interface to create units consists of:
![Units UML](_static/img/units.png)
All below class templates indirectly derive from a `scaled_unit` class template and satisfy a
`Unit` concept:
- `unit`
- Defines a new unnamed, in most cases coherent derived unit of a specific derived
dimension and it should be passed in this dimension's definition.
- `named_unit`
- Defines a named, in most cases base or coherent unit that is then passed to a dimension's
definition.
- A named unit may be used by other units defined with the prefix of the same type, unless
`no_prefix` is provided for `PrefixFamily` template parameter (in such a case it is impossible
to define a prefixed unit based on this one).
- `named_scaled_unit`
- Defines a new named unit that is a scaled version of another unit.
- Such unit can be used by other units defined with the prefix of the same type, unless
`no_prefix` is provided for `PrefixFamily` template parameter (in such a case it is impossible
to define a prefixed unit based on this one).
- `prefixed_unit`
- Defines a new unit that is a scaled version of another unit by the provided prefix.
- It is only possible to create such a unit if the given prefix type matches the one defined
in a reference unit.
- `deduced_unit`
- Defines a new unit with a deduced ratio and symbol based on the recipe from the provided
derived dimension.
- The number and order of provided units should match the recipe of the derived dimension.
- All of the units provided should also be a named ones so it is possible to create a deduced
symbol text.
Some of the above types depend on `PrefixFamily` and `no_prefix`. `PrefixFamily` is a concept that
is defined as:
```cpp
template<typename T>
concept PrefixFamily = std::derived_from<T, prefix_family>;
```
where `prefix_family` is just an empty tag type used to identify the beginning of prefix types
hierarchy and `no_prefix` is one of its children:
```cpp
struct prefix_family {};
struct no_prefix : prefix_family {};
```
Concrete prefix derives from a `prefix` class template:
```cpp
template<typename Child, PrefixFamily PF, basic_fixed_string Symbol, Ratio R>
requires (!std::same_as<PF, no_prefix>)
struct prefix;
```
You could notice that both units and above `prefix` class template take `Child` as a first
template parameter. `mp-units` library heavily relies on CRTP (Curiously Recurring Template
Parameter) idiom to provide the best user experience in terms of readability of compilation
errors and during debugging. It is possible thanks to the downcasting facility described later
in the design documentation.
Coming back to units, here are a few examples of unit definitions:
```cpp
namespace units::physical::si {
// prefixes
struct prefix : prefix_family {};
struct centi : units::prefix<centi, prefix, "c", ratio<1, 1, -2>> {};
struct kilo : units::prefix<kilo, prefix, "k", ratio<1, 1, 3>> {};
// length
struct metre : named_unit<metre, "m", prefix> {};
struct centimetre : prefixed_unit<centimetre, centi, metre> {};
struct kilometre : prefixed_unit<kilometre, kilo, metre> {};
// time
struct second : named_unit<second, "s", prefix> {};
struct hour : named_scaled_unit<hour, "h", no_prefix, ratio<3600>, second> {};
// speed
struct metre_per_second : unit<metre_per_second> {};
struct kilometre_per_hour : deduced_unit<kilometre_per_hour, dim_velocity, kilometre, hour> {};
}
namespace units::physical::us {
// length
struct yard : named_scaled_unit<yard, "yd", no_prefix, ratio<9'144, 10'000>, si::metre> {};
struct mile : named_scaled_unit<mile, "mi", no_prefix, ratio<1'760>, yard> {};
// speed
struct mile_per_hour : deduced_unit<mile_per_hour, si::dim_velocity, mile, si::hour> {};
}
```
Please note that thanks to C++20 features we are able to provide all information about the unit
(including text output) in a single line of its type definition. There is no need to specialize
additional type traits or use preprocessor macros.
## `Dimension`
`Dimension` is either a `BaseDimension` or a `DerivedDimension`:
```cpp
template<typename T>
concept Dimension = BaseDimension<T> || DerivedDimension<T>;
```
### `BaseDimension`
According to ISO 80000 a base quantity is a quantity in a conventionally chosen subset of a
given system of quantities, where no quantity in the subset can be expressed in terms of the
other quantities within that subset. They are referred to as being mutually independent since a
base quantity cannot be expressed as a product of powers of the other base quantities. Base unit
is a measurement unit that is adopted by convention for a base quantity in a specific system of
units.
`base_dimension` represents a dimension of a base quantity and is identified with a pair of
an unique compile-time text describing the dimension symbol and a base unit adopted for this
dimension:
```cpp
template<basic_fixed_string Symbol, Unit U>
requires U::is_named
struct base_dimension {
static constexpr auto symbol = Symbol;
using base_unit = U;
};
```
Pair of symbol and unit template parameters form an unique identifier of the base dimension.
These identifiers provide total ordering of exponents of base dimensions in a derived dimension.
The SI physical units system defines 7 base dimensions:
```cpp
namespace units::physical::si {
struct dim_length : base_dimension<"L", metre> {};
struct dim_mass : base_dimension<"M", kilogram> {};
struct dim_time : base_dimension<"T", second> {};
struct dim_electric_current : base_dimension<"I", ampere> {};
struct dim_thermodynamic_temperature : base_dimension<"Θ", kelvin> {};
struct dim_substance : base_dimension<"N", mole> {};
struct dim_luminous_intensity : base_dimension<"J", candela> {};
}
```
All other derived quantities of SI are composed from those.
There are two reasons why a `base_dimension` gets a unit as its template parameter. First, the
base unit is needed for the text output of unnamed derived units. Second, there is more than
one system of physical units. For example CGS definitions look as follows:
```cpp
namespace units::physical::cgs {
using si::centimetre;
using si::gram;
using si::second;
struct dim_length : base_dimension<"L", centimetre> {};
struct dim_mass : base_dimension<"M", gram> {};
using si::dim_time;
}
```
Equivalent base dimensions in different systems have the same symbol identifier and get units
from the same hierarchy (with the same reference in `scaled_unit`). Thanks to that we have
the ability to explicitly cast quantities of the same dimension from different systems or
even mix them in one `derived_dimension` definition.
### `DerivedDimension`
According to ISO 80000 a derived quantity is a quantity, in a system of quantities, defined in
terms of the base quantities of that system. Dimension of such quantity is an expression of the
dependence of a quantity on the base quantities of a system of quantities as a product of
powers of factors corresponding to the base quantities, omitting any numerical factors. A power
of a factor is the factor raised to an exponent. Each factor is the dimension of a base
quantity.
A derived dimension used internally in a library framework is implemented as a type-list like
type that stores an ordered list of exponents of one or more base dimensions:
```cpp
namespace detail {
template<Exponent E, Exponent... ERest>
requires (BaseDimension<typename E::dimension> && ... && BaseDimension<typename ERest::dimension>)
struct derived_dimension_base;
}
```
A derived dimension can be formed from multiple exponents (i.e. speed is represented as
`exp<L, 1>, exp<T, -1>`). It is also possible to form a derived dimension with only one exponent
(i.e. frequency is represented as just `exp<T, -1>`).
Exponents are implemented with `exp` class template that provides an information about a single
dimension and its (possibly fractional) exponent in a derived dimension.
```cpp
template<Dimension Dim, std::intmax_t Num, std::intmax_t Den = 1>
struct exp {
using dimension = Dim;
static constexpr std::intmax_t num = Num;
static constexpr std::intmax_t den = Den;
};
```
In order to be able to perform computations on an arbitrary set of exponents,
`derived_dimension_base` class template have to obey the following rules:
- it contains only base dimensions in the list of exponents,
- base dimensions are not repeated in a list (the exponent of each base dimension is provided
at most once),
- exponents of base dimensions are consistently ordered,
- in case the numerator of the exponent equals zero such base dimension is erased from the list.
Above is needed for the framework to provide dimensional analysis. However, sometimes it is
useful to define derived units in terms of other derived units. To support this both a base
dimension and a derived dimension can be provided to `exp` class template.
As it was stated above `derived_dimension_base` is a private utility of the framework. In order
to define a new derived dimension the user has to instantiate the following class template:
```cpp
template<typename Child, Unit U, Exponent E, Exponent... ERest>
struct derived_dimension : downcast_child<Child, typename detail::make_dimension<E, ERest...>> {
using recipe = exp_list<E, ERest...>;
using coherent_unit = U;
using base_units_ratio = /* see below */;
};
```
There are a few important differences between `detail::derived_dimension_base` and
`derived_dimension`. First, the latter one gets the coherent unit of the derived dimension.
According to ISO 80000 a coherent unit is a unit that, for a given system of quantities and for
a chosen set of base units, is a product of powers of base units with no other proportionality
factor than one.
The other difference is that `derived_dimension` allows to provide other derived dimensions in
the list of its exponents. This is called a "recipe" of the dimension and among others is used
to print unnamed coherent units of this dimension.
In case a derived dimension appears on the list of exponents, such derived dimension will be
unpacked, sorted, and consolidated by a `detail::make_dimension` helper to form a valid list
of exponents of only base dimensions later provided to `detail::derived_dimension_base`.
Sometimes units of equivalent quantities in different systems of units do not share the same
reference so they cannot be easily converted to each other. An example can be a pressure for
which a coherent unit in SI is pascal and in CGS barye. Those two units are not directly
related with each other with some ratio. As they both are coherent units of their dimensions,
the ratio between them is directly determined by the ratios of base units defined in base
dimensions end their exponents in the derived dimension recipe. To provide interoperability of
such quantities of different systems `base_units_ratio` is being used. The result of the
division of two `base_units_ratio` of two quantities of equivalent dimensions in two different
systems gives a ratio between their coherent units. Alternatively, the user would always have to
directly define a barye in terms of pascal or vice versa.
Below are a few examples of derived dimension definitions:
```cpp
namespace units::physical::si {
struct dim_velocity : derived_dimension<dim_velocity, metre_per_second,
exp<dim_length, 1>, exp<dim_time, -1>> {};
struct dim_acceleration : derived_dimension<dim_acceleration, metre_per_second_sq,
exp<dim_length, 1>, exp<dim_time, -2>> {};
struct dim_force : derived_dimension<dim_force, newton,
exp<dim_mass, 1>, exp<dim_acceleration, 1>> {};
struct dim_energy : derived_dimension<dim_energy, joule,
exp<dim_force, 1>, exp<dim_length, 1>> {};
struct dim_power : derived_dimension<dim_power, watt,
exp<dim_energy, 1>, exp<dim_time, -1>> {};
}
```
If as a result of dimensional computation the library framework will generate a derived
dimension that was not predefined by the user than the instance of
`unknown_dimension<Exponent...>`. The coherent unit of such an unknown dimension is
`scaled_unit<ratio<1>, unknown_coherent_unit>`.
## `Quantity`
`quantity` is a class template that expresses the quantity/amount of a specific dimension
expressed in a specific unit of that dimension:
```cpp
template<Dimension D, UnitOf<D> U, Scalar Rep = double>
class quantity
```
`quantity` provides a similar interface 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<Dimension D, UnitOf<D> U, Scalar Rep = double>
class quantity {
public:
using dimension = D;
using unit = U;
using rep = Rep;
[[nodiscard]] static constexpr quantity one() noexcept;
// ...
};
template<typename D1, typename U1, typename Rep1, typename D2, typename U2, typename Rep2>
requires detail::basic_arithmetic<Rep1, Rep2> && equivalent_dim<D1, dim_invert<D2>>
[[nodiscard]] constexpr Scalar auto operator*(const quantity<D1, U1, Rep1>& lhs,
const quantity<D2, U2, Rep2>& rhs);
template<typename D1, typename U1, typename Rep1, typename D2, typename U2, typename Rep2>
requires detail::basic_arithmetic<Rep1, Rep2> && (!equivalent_dim<D1, dim_invert<D2>>)
[[nodiscard]] constexpr Quantity auto operator*(const quantity<D1, U1, Rep1>& lhs,
const quantity<D2, U2, Rep2>& rhs);
template<Scalar Value, typename D, typename U, typename Rep>
requires std::magma<std::ranges::divided_by, Value, Rep>
[[nodiscard]] constexpr Quantity auto operator/(const Value& v,
const quantity<D, U, Rep>& q);
template<typename D1, typename U1, typename Rep1, typename D2, typename U2, typename Rep2>
requires detail::basic_arithmetic<Rep1, Rep2> && equivalent_dim<D1, D2>
[[nodiscard]] constexpr Scalar auto operator/(const quantity<D1, U1, Rep1>& lhs,
const quantity<D2, U2, Rep2>& rhs);
template<typename D1, typename U1, typename Rep1, typename D2, typename U2, typename Rep2>
requires detail::basic_arithmetic<Rep1, Rep2> && (!equivalent_dim<D1, D2>)
[[nodiscard]] constexpr Quantity AUTO operator/(const quantity<D1, U1, Rep1>& lhs,
const quantity<D2, U2, Rep2>& rhs);
```
Additional functions provide the support for operations that result in a different dimension
type than those of their arguments. `equivalent_dim` constraint requires two dimensions to be
either the same or have convertible units of base dimension (with the same reference unit).
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 SG6 (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.
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.
To simplify writing efficient generic code quantities of each dimension have associated:
1. Concept (i.e. `units::Length`) that matches a length dimension of any physical systems.
2. Per-system quantity alias (i.e. `units::physical::si::length<Unit, Rep>` for
`units::quantity<units::physical::si::dim_length, Unit, Rep>`).
Also, to help instantiate quantities with compile-time known values every unit in the library
has an associated UDL. For example:
```cpp
namespace si::inline literals {
// m
constexpr auto operator"" q_m(unsigned long long l) { return length<metre, std::int64_t>(l); }
constexpr auto operator"" q_m(long double l) { return length<metre, long double>(l); }
// km
constexpr auto operator"" q_km(unsigned long long l) { return length<kilometre, std::int64_t>(l); }
constexpr auto operator"" q_km(long double l) { return length<kilometre, long double>(l); }
}
```
### `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 (`Dimension`, `Unit`, or `Rep`):
```cpp
template<Quantity To, typename D, typename U, typename Rep>
requires QuantityOf<To, D> &&
detail::basic_arithmetic<std::common_type_t<typename To::rep, Rep, intmax_t>>
[[nodiscard]] constexpr auto quantity_cast(const quantity<D, U, Rep>& q);
template<Dimension ToD, typename D, typename U, typename Rep>
requires equivalent_dim<ToD, D>
[[nodiscard]] constexpr auto quantity_cast(const quantity<D, U, Rep>& q);
template<Unit ToU, typename D, typename U, typename Rep>
requires UnitOf<ToU, D>
[[nodiscard]] constexpr auto quantity_cast(const quantity<D, U, Rep>& q);
template<Scalar ToRep, typename D, typename U, typename Rep>
requires detail::basic_arithmetic<std::common_type_t<ToRep, Rep, intmax_t>>
[[nodiscard]] constexpr auto quantity_cast(const quantity<D, U, Rep>& q);
```
## Text output
### Unit Symbol
The library tries its best to print a correct unit of the quantity. This is why it performs
a series of checks:
1. If the user predefined a unit with a `named_XXX_unit` class templates, the symbol provided
by the user will be used (i.e. `60 W`).
2. If a unit was created with a `deduced_unit` class template, the symbol of deduced unit is
printed (i.e. `70 km/h`).
3. Otherwise, the library tries to print a prefix and symbol of an unknown unit for this derived
dimension:
- prefix:
- if ratio of the scaled unit is `1`, than no prefix is being printed,
- otherwise, if `PrefixFamily` template parameter of a reference unit is different than
`no_prefix`, and if the ratio of scaled unit matches the ratio of a prefix of a specified
type, than the symbol of this prefix will be used,
- otherwise, non-standard ratio (i.e. `2 [60]Hz`) will be printed.
- symbol:
- if a reference unit has a user-predefined or deduced symbol, than this symbol it is being
printed,
- otherwise, the symbol is constructed from names and exponents of base dimensions
(i.e. `2 m/kg^2`).
### `operator<<`
`quantity::operator<<()` provides only a basic support to print a quantity. It prints its count
and a symbol separated with one space character.
### Text Formatting
`mp-units` supports new C++20 formatting facility (currently provided as a dependency on
[`fmt`](https://github.com/fmtlib/fmt) library). `parse()` member functions of
`fmt::formatter<units::quantity<Dimension, Unit, Rep>, CharT>` class template partial
specialization interprets the format specification as a `units-format-spec` according to the
following syntax:
```text
units-format-spec:
fill-and-align[opt] sign[opt] width[opt] precision[opt] units-specs[opt]
units-specs:
conversion-spec
units-specs conversion-spec
units-specs literal-char
literal-char:
any character other than { or }
conversion-spec:
% modifier[opt] type
modifier: one of
E O
type: one of
n q Q t %
```
The productions `fill-and-align`, `sign`, `width`, and `precision` are described in
[Format string](https://wg21.link/format.string.std) chapter of the C++ standard. Giving a
`precision` specification in the `units-format-spec` is valid only for `units::quantity` types
where the representation type `Rep` is a floating-point type. For all other `Rep` types, an
exception of type `format_error` is thrown if the `units-format-spec` contains a precision
specification. An `format_error` is also thrown if `sign` is provided with a `conversion-spec`
to print quantity unit but not its value.
Each conversion specifier `conversion-spec` is replaced by appropriate characters as described
in the following table:
| Specifier | Replacement |
|:---------:|---------------------------------------------------------------|
| `%n` | A new-line character |
| `%q` | The quantitys unit symbol |
| `%Q` | The quantitys numeric value (as if extracted via `.count()`) |
| `%t` | A horizontal-tab character |
| `%%` | A `%` character |
If the `units-specs` is omitted, the `quantity` object is formatted as if by streaming it to
`std::ostringstream os` and copying `os.str()` through the output iterator of the context with
additional padding and adjustments as specified by the format specifiers.
```cpp
std::string s = fmt::format("{:=>12}", 120q_km_per_h); // value of s is "====120 km/h"
```
## Improving user's experience
Most of the important design decisions in the library are dictated by the requirement of
providing the best user experience as possible.
Most of C++ libraries in the world use template aliases to provide a friendly name for a
developer. Unfortunately, such aliases are quickly lost in a compilation process and as a
result the potential error log contains a huge source type rather than a short alias for it.
The same can be observed during debugging of a code using template aliases.
Let's assume that we want to provide a user friendly name for a capacitance derived dimension.
Other libraries will do it in the following way:
```cpp
using dim_capacitance = detail::derived_dimension_base<exp<si::dim_electric_current, 2>,
exp<si::dim_length, -2>,
exp<si::dim_mass, -1>,
exp<si::dim_time, 4>>;
```
The above solution does provide a good developer's experience but a really poor one for the end
user. If we will get a compilation error message containing `dim_capacitance` in most cases
the compiler will print the following type instead of the alias:
```text
units::detail::derived_dimension_base<units::exp<units::physical::si::dim_electric_current, 2, 1>,
units::exp<units::physical::si::dim_length, -2, 1>, units::exp<units::physical::si::dim_mass, -1, 1>,
units::exp<units::physical::si::dim_time, 4, 1> >
```
You can notice that even this long syntax was carefully selected to provide quite good user
experience (some other units libraries produce a type that cannot easily fit on one slide)
but it is not questionable less readable than just `dim_capacitance`.
NOTE: To better understand how the framework works and not clutter the text and graphs with
long types in the following examples we will switch from `dim_capacitance` to `dim_area`.
The latter one has much shorter definition but the end result for both will be exactly the same.
User-friendly, short name printed by the compiler and the debugger.
To fix it we have to provide a strong type. As we do not have opaque/strong typedefs
in the language we have to use inheritance:
![UML](_static/img/downcast_1.png)
This gives us a nice looking strong type but does not solve the problem of how to switch from
a long instantiation of a `derived_dimension_base` class template that was generated by the
framework as a result of dimensional calculation to a child class assigned by the user for this
instantiation.
### Downcasting facility
To support this `mp-units` library introduces a new downcasting facility implemented fully as
a library feature. It creates 1-to-1 link between a long class template instantiation and a
strong type provided by the user. This means that only one child class can be created for a
specific base class template instantiation.
Downcasting facility is provided by injecting two classes into our hierarchy:
![UML](_static/img/downcast_2.png)
In the above example `dim_area` is a downcasting target (child class) and a specific
`detail::derived_dimension` class template instantiation is a downcasting source (base class).
```cpp
template<typename BaseType>
struct downcast_base {
using downcast_base_type = BaseType;
friend auto downcast_guide(downcast_base); // declaration only (no implementation)
};
```
`units::downcast_base` is a class that implements CRTP idiom, marks the base of downcasting
facility with a `downcast_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_child` and will return a
target type of the downcasting operation there.
```cpp
template<typename T>
concept Downcastable =
requires {
typename T::downcast_base_type;
} &&
std::derived_from<T, downcast_base<typename T::downcast_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_child : T {
friend auto downcast_guide(typename downcast_child::downcast_base) { return Target(); }
};
```
`units::downcast_child` 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.
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.
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 = decltype(detail::downcast_target_impl<T>());
```
`units::downcast` is used to obtain the target type of the downcasting operation registered
for a given instantiation in a base type. `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 one more simple helper alias provided that is used in the internal
library implementation:
```cpp
template<Downcastable T>
using downcast_base_t = T::downcast_base_type;
```
### `unknown_dimension<Exponent...>`
Sometimes dimensional calculation results with a class template instantiation that was not
predefined by the user in the downcasting facility. A typical example of such a case are
temporary results of calculations:
```cpp
units::Length auto d1 = 123q_m;
units::Time auto t1 = 10q_s;
units::Speed auto v1 = avg_speed(d1, t1);
auto temp1 = v1 * 50q_m; // intermediate unknown dimension
units::Speed auto v2 = temp1 / 100q_m; // back to known dimensions again
units::Length auto d2 = v2 * 60q_s;
```
To provide support to form an unknown derived dimension that could be than be converted to a
known one with a correct unit, and also to improve the user experience and clearly state that
it is an unknown dimension the library framework will provide an instance of:
```cpp
struct unknown_coherent_unit : unit<unknown_coherent_unit> {};
template<Exponent E, Exponent... ERest>
struct unknown_dimension : derived_dimension<unknown_dimension<E, ERest...>,
scaled_unit<ratio<1>, unknown_coherent_unit>,
E, ERest...> {
using coherent_unit = scaled_unit<ratio<1>, unknown_coherent_unit>;
};
```
with this the error log or a debugger breakpoint involving a `temp1` type will include:
```text
units::quantity<units::unknown_dimension<units::exp<units::physical::si::dim_length, 2, 1>,
units::exp<units::physical::si::dim_time, -1, 1> >, units::unknown_coherent_unit, long int>
```
## Extensibility
The library was designed with a simple extensibility in mind. It is easy to add new units,
dimensions, and prefixes. The systems of units are not closed (classes) but open (namespaces)
and can be easily extended, or its content can be partially/fully imported to other systems.
### Adding a new system with custom dimensions and units
A great example of a adding a whole new system can be a `data` system in the library which
adds support for digital information quantities. In summary it adds:
1. New prefix type and its prefixes:
```cpp
namespace units::data {
struct prefix : prefix_family {};
struct kibi : units::prefix<kibi, prefix, "Ki", ratio< 1'024>> {};
struct mebi : units::prefix<mebi, prefix, "Mi", ratio<1'048'576>> {};
}
```
2. New units for `information`:
```cpp
namespace units::data {
struct bit : named_unit<bit, "b", prefix> {};
struct kibibit : prefixed_unit<kibibit, kibi, bit> {};
struct byte : named_scaled_unit<byte, "B", prefix, ratio<8>, bit> {};
struct kibibyte : prefixed_unit<kibibyte, kibi, byte> {};
}
```
3. New base dimension, its concept, and quantity alias:
```cpp
namespace units::data {
struct dim_information : base_dimension<"information", bit> {};
template<typename T>
concept Information = QuantityOf<T, dim_information>;
template<Unit U, Scalar Rep = double>
using information = quantity<dim_information, U, Rep>;
}
```
4. UDLs for new units
```cpp
namespace units::data::inline literals {
// bits
constexpr auto operator"" q_b(unsigned long long l) { return information<bit, std::int64_t>(l); }
constexpr auto operator"" q_Kib(unsigned long long l) { return information<kibibit, std::int64_t>(l); }
// bytes
constexpr auto operator"" q_B(unsigned long long l) { return information<byte, std::int64_t>(l); }
constexpr auto operator"" q_KiB(unsigned long long l) { return information<kibibyte, std::int64_t>(l); }
}
```
5. A new `bitrate` derived dimension, its units, concept, quantity helper, and UDLs
```cpp
namespace units::data {
struct bit_per_second : unit<bit_per_second> {};
struct dim_bitrate : derived_dimension<dim_bitrate, bit_per_second, exp<dim_information, 1>, exp<si::dim_time, -1>> {};
struct kibibit_per_second : deduced_unit<kibibit_per_second, dim_bitrate, kibibit, si::second> {};
template<typename T>
concept Bitrate = QuantityOf<T, dim_bitrate>;
template<Unit U, Scalar Rep = double>
using bitrate = quantity<dim_bitrate, U, Rep>;
inline namespace literals {
// bits
constexpr auto operator"" q_b_per_s(unsigned long long l) { return bitrate<bit_per_second, std::int64_t>(l); }
constexpr auto operator"" q_Kib_per_s(unsigned long long l) { return bitrate<kibibit_per_second, std::int64_t>(l); }
}
}
```
### Using custom representations
In theory `quantity` can take any arithmetic-like type as a `Rep` template parameter. In
practice some interface is forced by numeric concepts.
To provide basic library functionality the type should satisfy the `Scalar` concept:
```cpp
template<typename T, typename U = T>
concept basic-arithmetic = // exposition only
std::magma<std::ranges::plus, T, U> &&
std::magma<std::ranges::minus, T, U> &&
std::magma<std::ranges::times, T, U> &&
std::magma<std::ranges::divided_by, T, U>;
template<typename T>
concept Scalar =
(!Quantity<T>) &&
(!WrappedQuantity<T>) &&
std::regular<T> &&
std::totally_ordered<T> &&
basic-arithmetic<T>;
```
Where `WrappedQuantity` is a concept that applies `Quantity<typename T::value_type>` recursively
on all nested types to check if `T` is not actually a wrapped quantity type (i.e. a vector or
matrix of quantities).
The above implies that the `Rep` type should provide at least:
- default constructor, destructor, copy-constructor, and copy-assignment operator
- `operator==(Rep, Rep)`, `operator!=(Rep, Rep)`
- `operator<(Rep, Rep)`, `operator>(Rep, Rep)`, `operator<=(Rep, Rep)`, `operator>=(Rep, Rep)`
- `operator-(Rep)`
- `operator+(Rep, Rep)`, `operator-(Rep, Rep)`, `operator*(Rep, Rep)`, `operator*(Rep, Rep)`
Above also requires that the `Rep` should be implicitly convertible from integral types
(i.e. `int`) so a proper implicit converting constructor should be provided.
Moreover, in most cases to observe expected behavior `Rep` will have to be registered as a
floating-point representation type by specializing `units::treat_as_floating_point` type
trait:
```cpp
template<typename Rep>
inline constexpr bool treat_as_floating_point;
```
An example of such a type can be found in [measurement example](../example/measurement.cpp).
However, as written above this will enable only a basic functionality of the library. In case
additional `quantity` operations are needed the user may opt-in to any of them by providing
the equivalent operation for `Rep` type. Here is an additional list of opt-in operations:
- `operator++()`
- `operator++(int)`
- `operator--()`
- `operator--(int)`
- `operator+=(Rep)`
- `operator-=(Rep)`
- `operator*=(Rep)`
- `operator/=(Rep)`
- `operator%=(Rep)`
- `operator%(Rep, Rep)`
`quantity` also has 4 static functions `zero()`, `one()`, `min()`, and `max()` which can
be enabled by providing a specialization of `quantity_values` type trait for `Rep` type:
```cpp
template<Scalar Rep>
struct quantity_values;
```
## FAQ
1. Why all UDLs are prefixed with `q_` instead of just using unit symbol?
Usage of only unit symbols in UDLs would be a preferred approach (less to type, easier to
understand and maintain). However, while increasing the coverage for the library we learned
that there are a lot unit symbols that conflict with built-in types or numeric extensions.
A few of those are: `F` (farad), `J` (joule), `W` (watt), `K` (kelvin), `d` (day), `l` or
`L` (litre), `erg`, `ergps`. For a while we had to used `_` prefix to make the library work
at all but at some point we had to unify the naming and we came up with `q_` prefix which
results in a creation of quantity of a provided unit.
2. Why dimensions depend on units and not vice versa?
Most of the libraries define units in terms of dimensions and this was also an initial
approach for this library. However it turns out that for such a design it is hard to provide
support for all the required scenarios.
The first of them is to support multiple unit systems (like SI, CGS, ...) where each of
can have a different base unit for the same dimension. Base quantity of dimension length in
SI has to know that it should use `m` to print the unit symbol to the text output, while
the same dimension for CGS should use `cm`. Also it helps in conversions among those systems.
The second one is to support natural units where more than one dimension can be measured
with the same unit (i.e. `GeV`). Also if someone will decide to implement a systems where
SI quantities of the same kind are expressed as different dimensions (i.e. height, width,
and depth) all of them will just be measured in meters.
3. Why do we spell `metre` instead of `meter`?

View File

@ -13,19 +13,4 @@ Design Deep Dive
design/directories
design/quantity
The Downcasting Facility
------------------------
..
http://www.nomnoml.com
[detail::derived_dimension_base<exp<si::dim_length, 2>>]<:-[dim_area]
..
http://www.nomnoml.com
[downcast_base<detail::derived_dimension_base<exp<si::dim_length, 2>>>]<:-[detail::derived_dimension_base<exp<si::dim_length, 2>>]
[detail::derived_dimension_base<exp<si::dim_length, 2>>]<:-[downcast_child<dim_area, detail::derived_dimension_base<exp<si::dim_length, 2>>>]
[downcast_child<dim_area, detail::derived_dimension_base<exp<si::dim_length, 2>>>]<:-[dim_area]
design/downcasting

163
docs/design/downcasting.rst Normal file
View File

@ -0,0 +1,163 @@
.. namespace:: units
The Downcasting Facility
========================
Problem statement
-----------------
Most of the C++ libraries in the world use template aliases to provide a friendly name for a
developer. Unfortunately, such aliases are quickly lost in a compilation process and as a
result the potential error log contains a huge source type rather than a short alias for it.
The same can be observed during debugging of a source code that use template aliases.
Let's assume that we want to provide a user friendly name for a derived dimension of capacitance
quantity. Other libraries will do it in the following way::
using dim_capacitance = detail::derived_dimension_base<exp<si::dim_electric_current, 2>,
exp<si::dim_length, -2>,
exp<si::dim_mass, -1>,
exp<si::dim_time, 4>>;
The above solution does provide a good developer's experience but a really poor one for the end
user. If we will get a compilation error message containing `dim_capacitance` in most cases
the compiler will print the following type instead of the alias::
units::detail::derived_dimension_base<units::exp<units::physical::si::dim_electric_current, 2, 1>,
units::exp<units::physical::si::dim_length, -2, 1>, units::exp<units::physical::si::dim_mass, -1, 1>,
units::exp<units::physical::si::dim_time, 4, 1> >
You can notice that in case of **mp-units** even this long syntax was carefully selected to
provide quite good user experience (some other units libraries produce a type that cannot easily
fit on one slide) but it is not questionable less readable than just `dim_capacitance`.
.. note::
To better understand how the framework works and not clutter the text and graphs with
long types in the following examples we will switch from `dim_capacitance` to `dim_area`.
The latter one has much shorter definition but the end result for both will be exactly the same -
user-friendly, short name printed by the compiler and the debugger.
As we lack opaque/strong typedefs in the C++ language the only way to improve our case is
to use inheritance:
.. image:: /_static/img/downcast_1.png
:align: center
..
http://www.nomnoml.com
[derived_dimension_base<exp<si::dim_length, 2>>]<:-[dim_area]
This gives us a nice looking strong type when directly used by the user. However, we just got
ourselves into problems. The library's framework does not know how to switch from a long
instantiation of a `derived_dimension_base` class template that was generated as a result
of dimensional calculation to a nicely named child class assigned by the user for this
instantiation.
How it works?
-------------
To support this **mp-units** library introduces a new downcasting facility implemented fully
as a library feature. It creates 1-to-1 link between a long class template instantiation and a
strong type provided by the user. This provides automatic type substitution mechanism in the
framework.
.. important::
The above 1-1 correspondence means that only one child class can be provided for a specific
base class template instantiation. If a user will try to assign another child class to
already used base class template instantiation the program will not compile.
The downcasting facility is provided by injecting two classes into our hierarchy:
.. image:: /_static/img/downcast_2.png
:align: center
..
http://www.nomnoml.com
[downcast_base<detail::derived_dimension_base<exp<si::dim_length, 2>>>]<:-[detail::derived_dimension_base<exp<si::dim_length, 2>>]
[detail::derived_dimension_base<exp<si::dim_length, 2>>]<:-[downcast_child<dim_area, detail::derived_dimension_base<exp<si::dim_length, 2>>>]
[downcast_child<dim_area, detail::derived_dimension_base<exp<si::dim_length, 2>>>]<:-[dim_area]
In the above example:
- ``dim_area`` is a downcasting target (child class)
- `detail::derived_dimension_base` class template instantiation is a downcasting source (base class)
- `downcast_base` is a class that implements :abbr:`CRTP (Curiously Recurring Template Pattern)`
idiom, stores the base of a downcasting operation in a ``downcast_base_type`` member type,
and provides only a Hidden Friend non-member function declaration of ``downcast_guide`` which is an
:abbr:`ADL (Argument Dependent Lookup)` entry point for the downcasting operation::
template<typename BaseType>
struct downcast_base {
using downcast_base_type = BaseType;
friend auto downcast_guide(downcast_base); // declaration only (no implementation)
};
.. important::
An important design point here is that this friend function does not return any specific type
in its declaration and no definition is provided at this point.
- `downcast_child` is another :abbr:`CRTP (Curiously Recurring Template Pattern)` class template
that defines the implementation of a non-member friend function of the `downcast_base` class
template::
template<typename Target, Downcastable T>
struct downcast_child : T {
friend auto downcast_guide(typename downcast_child::downcast_base) { return Target(); }
};
This is the place where the actual return type of the ``downcast_guide`` function is provided
which serves as a target type of the downcasting operation.
In the above class template definition `Downcastable` is a concepts that verifies if a type
implements and can be used in a downcasting facility::
template<typename T>
concept Downcastable =
requires {
typename T::downcast_base_type;
} &&
std::derived_from<T, downcast_base<typename T::downcast_base_type>>;
With such :abbr:`CRTP (Curiously Recurring Template Pattern)` types the only thing the user
has to do in order to register a new type in the downcasting facility is to publicly derive
from `downcast_child` and pass this type as the first template argument of the `downcast_child`
class template.
Until now we scoped on how we define the base and target of a downcasting operation. To
perform the actual downcasting operation a dedicated alias template is provided::
template<Downcastable T>
using downcast = decltype(detail::downcast_target_impl<T>());
`downcast` is used to obtain the target type of the downcasting operation registered for a
given instantiation in a base type. `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 trait returning a provided base class::
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();
}
}

View File

@ -8,3 +8,4 @@ Examples
examples/avg_speed
examples/measurement
examples/linear_algebra
examples/box_example

View File

@ -0,0 +1,7 @@
box_example
===========
.. literalinclude:: ../../example/box_example.cpp
:caption: box_example.cpp
:start-at: #include
:linenos:

View File

@ -45,3 +45,27 @@ Approach
5. No external dependencies
6. Possibility to be standardized as a freestanding part of the C++ Standard
Library
With the User's Experience in Mind
----------------------------------
Most of the important design decisions in the library are dictated by the requirement of
providing the best user experience as possible. Other C++ physical units libraries are
"famous" for their huge error messages (one line of the error log often do not fit on one
slide). The ultimate goal of **mp-units** is to improve this and make compile-time errors
and debugging as easy and user-friendly as possible.
To achieve this goal several techniques are applied:
- usage of C++20 concepts,
- using strong types for framework entities (instead of type aliases),
- limiting the number of template arguments to the bare minimum,
- :ref:`The Downcasting Facility`.
.. important::
In many generic C++ libraries compile-time errors do not happen often. It is hard to
break ``std::string`` or ``std::vector`` in a way it won't compile with a huge error
log. Physical Units libraries are different. **Generation of compile-time errors
is the main reason to create such a library.**

View File

@ -9,9 +9,9 @@ Use Cases
using namespace units::physical;
.. toctree::
:maxdepth: 2
:maxdepth: 1
use_cases/unknown_units_and_dimensions
use_cases/unknown_dimensions
use_cases/legacy_interfaces
use_cases/custom_representation_types
use_cases/linear_algebra

View File

@ -3,17 +3,203 @@
Extending the Library
=====================
The library was designed with a simple extensibility in mind. It is easy to add new units,
dimensions, and prefixes. The systems of units are not closed (classes) but open (namespaces)
and can be easily extended, or its content can be partially/fully imported to other systems.
Custom Units
------------
It might happen that the user would like to use a unit that is not predefined by the library
or is predefined but the user would like to name it differently, assign a different symbol
to existing unit, or make it a base unit for prefixes.
Defining a New Unit
^^^^^^^^^^^^^^^^^^^
My working desk is of ``180 cm x 60 cm`` which gives an area of ``0.3 m²``. I would like to
make it a unit of area for my project::
struct desk : named_scaled_unit<desk, "desk", no_prefix, ratio<3, 10>, si::square_metre> {};
With the above I can define a quantity with the area of ``2 desks``::
auto d1 = si::area<desk>(2);
In case I feel it is too verbose to type the above every time I can easily create a custom
alias or an :abbr:`UDL (User Defined Literal)`::
// alias with fixed integral representation
using desks = si::area<desk, std::int64_t>;
// UDLs
constexpr auto operator"" _d(unsigned long long l) { return si::area<desk, std::int64_t>(l); }
constexpr auto operator"" _d(long double l) { return si::area<desk, long double>(l); }
Right now I am fully set up for my project and can start my work of tracking the area taken
by my desks::
auto d1 = si::area<desk>(2);
auto d2 = desks(3);
auto d3 = 1_d;
auto sum = d1 + d2 + d3;
std::cout << "Area: " << sum << '\n'; // prints 'Area: 6 desk'
In case I would like to check how much area ``6 desks`` take in SI units::
auto sum_si = quantity_cast<si::square_metre>(sum);
std::cout << "Area (SI): " << sum_si << '\n'; // prints 'Area (SI): 1.8 m²'
Enabling a Unit for Prefixing
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In case I decide it is reasonable to express my desks with SI prefixes the only thing I have
to change in the above code is to replace `no_prefix` with `si_prefix`::
struct desk : named_scaled_unit<desk, "desk", si::prefix, ratio<3, 10>, si::square_metre> {};
Now I can define a new unit named ``kilodesk``::
struct kilodesk : prefixed_unit<kilodesk, si::kilo, desk> {};
static_assert(3_d * 1000 == si::area<kilodesk>(3));
But maybe SI prefixes are not good for me. Maybe I always pack ``6`` desks into one package
for shipment and ``40`` such packages fit into my lorry. To express this with prefixes a new
prefix family and prefixes are needed::
struct shipping_prefix : prefix_family {};
struct package : prefix<package, shipping_prefix, "pkg", ratio<6>> {};
struct lorry : prefix<lorry, shipping_prefix, "lorry", ratio<6 * 40>> {};
Now we can use it for our unit::
struct desk : named_scaled_unit<desk, "desk", shipping_prefix, ratio<3, 10>, si::square_metre> {};
struct packagedesk : prefixed_unit<packagedesk, package, desk> {};
struct lorrydesk : prefixed_unit<lorrydesk, lorry, desk> {};
With the above::
static_assert(6_d == si::area<packagedesk>(1));
static_assert(240_d == si::area<lorrydesk>(1));
std::cout << "Area: " << quantity_cast<packagedesk>(sum) << '\n'; // prints 'Area: 1 pkgdesk'
It is important to notice that with the definition of a custom prefix I did not loose SI
units compatibility. If I want to calculate how much area I can cover with desks delivered
by ``3 lorries`` I can do the following::
auto area = quantity_cast<si::square_metre>(si::area<lorrydesk>(3));
std::cout << "Area: " << area << '\n'; // prints 'Area: 216 m²'
Custom Dimensions
-----------------
Custom Base Dimensions
^^^^^^^^^^^^^^^^^^^^^^
There are cases were a custom unit is not enough and the user would like to define custom
dimensions. The most common case is to define a new derived dimension from other dimensions
already predefined in various systems. But in **mp-units** library it is also really easy to
define a new base dimension for a custom units system.
Custom Derived Dimensions
^^^^^^^^^^^^^^^^^^^^^^^^^
In case I want to track how many desks I can produce over time or what is the consumption
rate of wood during production I need to define a new derived dimension together with its
coherent unit::
// coherent unit must apply to the system rules (in this case SI)
struct square_metre_per_second : unit<square_metre_per_second> {};
// new derived dimensions
struct dim_desk_rate : derived_dimension<dim_desk_rate, square_metre_per_second,
exp<si::dim_area, 1>, exp<si::dim_time, -1>> {};
// our unit of interest for a new derived dimension
struct desk_per_hour : deduced_unit<desk_per_hour, dim_desk_rate, desk, si::hour> {};
// a quantity of our dimension
template<Unit U, Scalar Rep = double>
using desk_rate = quantity<dim_desk_rate, U, Rep>;
// a concept matching the above quantity
template<typename T>
concept DeskRate = QuantityOf<T, dim_desk_rate>;
With the above we can now check what is the production rate::
DeskRate auto rate = quantity_cast<desk_per_hour>(3._d / 20q_min);
std::cout << "Desk rate: " << rate << '\n'; // prints 'Desk rate: 9 desk/h'
and how much wood is being consumed over a unit of time::
auto wood_rate = quantity_cast<square_metre_per_second>(rate);
std::cout << "Wood rate: " << wood_rate << '\n'; // prints 'Wood rate: 0.00075 m²/s'
Custom Base Dimensions
^^^^^^^^^^^^^^^^^^^^^^
In case I want to monitor what is the average number of people sitting by one desk in
a customer's office I would need a unit called ``person_per_desk`` of a new derived
dimension. However, our library does not know what a ``person`` is. For this I need to
define a new base dimension, its units, quantity helper, concept, and UDLs::
struct person : named_unit<person, "person", no_prefix> {};
struct dim_people : base_dimension<"people", person> {};
template<Unit U, Scalar Rep = double>
using people = quantity<dim_people, U, Rep>;
template<typename T>
concept People = QuantityOf<T, dim_people>;
constexpr auto operator"" _p(unsigned long long l) { return people<person, std::int64_t>(l); }
constexpr auto operator"" _p(long double l) { return people<person, long double>(l); }
With the above we can now define a new derived dimension::
struct person_per_square_metre : unit<person_per_square_metre> {};
struct dim_occupancy_rate : derived_dimension<dim_occupancy_rate, person_per_square_metre,
exp<dim_people, 1>, exp<si::dim_area, -1>> {};
struct person_per_desk : deduced_unit<person_per_desk, dim_occupancy_rate, person, desk> {};
template<Unit U, Scalar Rep = double>
using occupancy_rate = quantity<dim_occupancy_rate, U, Rep>;
template<typename T>
concept OccupancyRate = QuantityOf<T, dim_occupancy_rate>;
Now we can play with our new feature::
People auto employees = 1450._p;
auto office_desks = 967_d;
OccupancyRate auto occupancy = employees / office_desks;
std::cout << "Occupancy: " << occupancy << '\n'; // prints 'Occupancy: 1.49948 person/desk'
Custom Systems
--------------
Being able to extend predefined systems is a mandatory feature of any physical
units library. Fortunately, for **mp-units** there is nothing special to do here.
A system is defined in terms of its base dimensions. If you are using only SI
base dimensions then you are in the boundaries of the SI system. If you are
adding new base dimensions, like we did in the `Custom Base Dimensions`_
chapter, you are defining a new system.
In **mp-units** library a custom system can either be constructed from
unique/new custom base dimensions or reuse dimensions of other systems. This
allows extending, mixing, reuse, and interoperation between different systems.
.. seealso::
More information on extending the library can be found in the
:ref:`Using Custom Representation Types` chapter.

View File

@ -0,0 +1,109 @@
.. namespace:: units
Working with Unknown Dimensions and Their Units
===============================================
From time to time the user of this library will face an `unknown_dimension` and
`unknown_coherent_unit` types. This chapters describes their purpose and usage in
detail.
What is an unknown dimension?
-----------------------------
As we learned in the :ref:`Dimensions` chapter, in most cases the result of multiplying
or dividing two quantities of specific dimensions is a quantity of yet another dimension.
If such a resulting dimension is predefined by the user (and a proper header file with its
definition is included in the current translation unit) :ref:`The Downcasting Facility`
will determine its type. The same applies to the resulting unit. For example:
.. code-block::
:emphasize-lines: 3,7-9
#include <units/physical/si/length.h>
#include <units/physical/si/time.h>
#include <units/physical/si/speed.h>
using namespace units::physical::si;
constexpr auto result = 144q_km / 2q_h;
static_assert(std::is_same_v<decltype(result)::dimension, dim_velocity>);
static_assert(std::is_same_v<decltype(result)::unit, kilometre_per_hour>);
However, if the resulting dimension is not predefined by the user the library framework
will create an instance of an `unknown_dimension`. The coherent unit of such an unknown
dimension is an `unknown_coherent_unit`. Let's see what happens with our example when
we forget to include a header file with the resulting dimension definition:
.. code-block::
:emphasize-lines: 3,9,11
#include <units/physical/si/length.h>
#include <units/physical/si/time.h>
// #include <units/physical/si/speed.h>
using namespace units::physical::si;
constexpr auto result = 144q_km / 2q_h;
static_assert(std::is_same_v<decltype(result)::dimension,
unknown_dimension<exp<dim_length, 1>, exp<dim_time, -1>>>);
static_assert(std::is_same_v<decltype(result)::unit,
scaled_unit<ratio<1, 36, 1>, unknown_coherent_unit>>);
Operations On Unknown Dimensions And Their Units
------------------------------------------------
For some cases we can eliminate the need to predefine a specific dimension and just use
the `unknown_dimension` instead. Let's play with the previous example a bit::
static_assert(result.count() == 72);
As we can see the value stored in this quantity can be easily obtained and contains a
correct result. However, if we try to print its value to the text output we will get::
std::cout << "Speed: " << result << '\n'; // prints 'Speed: 72 [1/36 × 10¹] m/s'
The output from above program should not be a surprise. It is an unknown dimensions with
a scaled unknown coherent unit. The library can't know what is the symbol of such unit
so it does its best and prints the unit in terms of units of base dimensions that formed
this particular unknown derived dimension.
In case we would like to print the result in terms of base units we can simply do the
following::
auto s = quantity_cast<unknown_coherent_unit>(result);
std::cout << "Speed: " << s << '\n'; // prints 'Speed: 20 m/s'
.. seealso::
Another good example of unknown dimension usage can be found in the
:ref:`box_example`::
std::cout << "float rise rate = " << box.fill_level(measured_mass) / fill_time << '\n';
Temporary Results
-----------------
In many cases there is nothing inherently wrong with having unknown dimensions and units
in your program. A typical example here are temporary results of a long calculation:
.. code-block::
:emphasize-lines: 5,7
auto some_long_calculation(Length auto d, Time auto t)
{
Speed auto s1 = avg_speed(d, t);
auto temp1 = s1 * 200q_km; // intermediate unknown dimension
Speed auto s2 = temp1 / 50q_km; // back to known dimensions again
Length auto d2 = s2 * 4q_h;
// ...
}
If a programmer wants to break the calculation to several lines/variables he/she does not
have to ensure that the intermediate results are of predefined dimensions or just a clear
science fiction :-) The final result will always be correct.

View File

@ -1,9 +0,0 @@
.. namespace:: units
Working with Unknown Units and Dimensions
=========================================
- what is an unknown unit?
- what is an unknown dimension?
- temporary result
- casting to the coherent unit