forked from mpusz/mp-units
Documentation updated
This commit is contained in:
@ -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"
|
||||
|
965
docs/DESIGN.md
965
docs/DESIGN.md
@ -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`:
|
||||
|
||||

|
||||
|
||||
`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:
|
||||
|
||||

|
||||
|
||||
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 quantity’s unit symbol |
|
||||
| `%Q` | The quantity’s 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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
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`?
|
@ -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
163
docs/design/downcasting.rst
Normal 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();
|
||||
}
|
||||
|
||||
}
|
@ -8,3 +8,4 @@ Examples
|
||||
examples/avg_speed
|
||||
examples/measurement
|
||||
examples/linear_algebra
|
||||
examples/box_example
|
||||
|
7
docs/examples/box_example.rst
Normal file
7
docs/examples/box_example.rst
Normal file
@ -0,0 +1,7 @@
|
||||
box_example
|
||||
===========
|
||||
|
||||
.. literalinclude:: ../../example/box_example.cpp
|
||||
:caption: box_example.cpp
|
||||
:start-at: #include
|
||||
:linenos:
|
@ -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.**
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
109
docs/use_cases/unknown_dimensions.rst
Normal file
109
docs/use_cases/unknown_dimensions.rst
Normal 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.
|
@ -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
|
Reference in New Issue
Block a user