diff --git a/doc/DESIGN.md b/doc/DESIGN.md index 9c3a1db9..2d543005 100644 --- a/doc/DESIGN.md +++ b/doc/DESIGN.md @@ -1,9 +1,9 @@ -# `mp-units` - A Units Library for C++ +# `mp-units` - Design Overview ## Summary -`Units` is a compile-time enabled Modern C++ library that provides compile-time dimensional +`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. @@ -46,48 +46,237 @@ static_assert(10km / 5km == 2); ## Basic Concepts -Below UML diagram shows the most important entities in the library design and how they relate to -each other: +The most important concepts in the library are `Unit`, `Dimension`, and `Quantity`: -![UML](units_uml.png) +![Design UML](design.png) -### `Dimensions` +`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. -`units::dimension` represents a derived dimension and is implemented as a type-list like type that -stores an ordered list of exponents of one or more base dimensions: +`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 -struct dimension : downcast_base> {}; +template +struct scaled_unit : downcast_base> { + using ratio = R; + using reference = U; +}; ``` -`units::Dimension` is a concept that is satisfied by a type that is empty and publicly -derived from `units::dimension` class template: +The above 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](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 `PrefixType` 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 `PrefixType` 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 `PrefixType` and `no_prefix`. `PrefixType` is a concept that +is defined as: ```cpp template -concept Dimension = - std::is_empty_v && - detail::is_dimension>; // exposition only +concept PrefixType = std::derived_from; ``` -#### `Exponents` +where `prefix_type` is just an empty tag type used to identify the beginning of prefix types +hierarchy and `no_prefix` is one of its children: -`units::Exponent` concept is satisfied if provided type is an instantiation of `units::exp` class -template: +```cpp +struct prefix_type {}; +struct no_prefix : prefix_type {}; +``` + +Concrete prefix derives from a `prefix` class template: + +```cpp +template + requires (!std::same_as) +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::si { + +// prefixes +struct prefix : prefix_type {}; +struct centi : units::prefix> {}; +struct kilo : units::prefix> {}; + +// length +struct metre : named_unit {}; +struct centimetre : prefixed_unit {}; +struct kilometre : prefixed_unit {}; +struct yard : named_scaled_unit, metre> {}; +struct mile : named_scaled_unit, yard> {}; + +// time +struct second : named_unit {}; +struct hour : named_scaled_unit, second> {}; + +// velocity +struct metre_per_second : unit {}; +struct kilometre_per_hour : deduced_unit {}; +struct mile_per_hour : deduced_unit {}; + +} +``` + +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 -concept Exponent = - detail::is_exp; // exposition only +concept Dimension = BaseDimension || DerivedDimension; ``` -`units::exp` provides an information about a single dimension and its (possibly fractional) -exponent in a derived dimension. +### `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 - requires BaseDimension || Dimension +template + 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::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 physical dimensions are derived from those. + +There are two reasons why `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::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 + requires (BaseDimension && ... && BaseDimension) +struct derived_dimension_base; + +} +``` + +A derived dimension can be formed from multiple exponents (i.e. velocity is represented as +`exp, exp`). It is also possible to form a derived dimension with only one exponent +(i.e. frequency is represented as just `exp`). + +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 struct exp { using dimension = Dim; static constexpr int num = Num; @@ -95,434 +284,236 @@ struct exp { }; ``` -Both a base dimension and a derived dimension can be provided to `units::exp` class template. +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. -`units::base_dimension` represents a base dimension and has assigned a unique compile-time text -describing the dimension name: +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 -struct base_dimension { - using name = Name; - using symbol = Symbol; +template +struct derived_dimension : downcast_child> { + using recipe = exp_list; + using coherent_unit = U; + using base_units_ratio = /* see below */; }; ``` -`units::BaseDimension` is a concept to match all types derived from `base_dimension` instantiations: +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 -template -concept BaseDimension = std::is_empty_v && - requires { - typename T::name; - typename T::symbol; - } && - std::derived_from>; +namespace units::si { + +struct dim_velocity : derived_dimension, exp> {}; + +struct dim_acceleration : derived_dimension, exp> {}; + +struct dim_force : derived_dimension, exp> {}; + +struct dim_energy : derived_dimension, exp> {}; + +struct dim_power : derived_dimension, exp> {}; + +} ``` -For example here is a list of SI base dimensions: +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`. The coherent unit of such an unknown dimension is +`scaled_unit, unknown_unit>`. -```cpp -struct base_dim_length : base_dimension<"length", "m"> {}; -struct base_dim_mass : base_dimension<"mass", "kg"> {}; -struct base_dim_time : base_dimension<"time", "s"> {}; -struct base_dim_current : base_dimension<"current", "A"> {}; -struct base_dim_temperature : base_dimension<"temperature", "K"> {}; -struct base_dim_substance : base_dimension<"substance", "mol"> {}; -struct base_dim_luminous_intensity : base_dimension<"luminous intensity", "cd"> {}; -``` -In order to be able to perform computations on arbitrary sets of base dimensions, an important -property of `units::dimension` is that its base dimensions: -- are not repeated in a list (each base dimension is provided at most once), -- are consistently ordered, -- having zero exponent are elided. +## `Quantity` - -#### `derived_dimension` - -Above design of dimensions is created with the ease of use for end users in mind. Compile-time -errors should provide as short as possible template instantiations strings that should be easy to -understand by every engineer. Also types visible in a debugger should be easy to understand. -That is why `units::dimension` type for derived dimensions always stores information about only -those base dimensions that are used to form that derived dimension. - -However, such an approach have some challenges: - -```cpp -constexpr Velocity auto v1 = 1_m / 1s; -constexpr Velocity auto v2 = 2 / 2s * 1m; - -static_assert(std::same_as); -static_assert(v1 == v2); -``` - -Above code, no matter what is the order of the base dimensions in an expression forming our result, -must produce the same `Velocity` type so that both values can be easily compared. In order to achieve -that, `dimension` class templates should never be instantiated manually but through a `derived_dimension` -helper: - -```cpp -template -struct derived_dimension : downcast_child::type> {}; -``` - -`Child` class template parameter is a part of a CRTP idiom and is used to provide a downcasting facility -described later in this document. - -So for example to create a `velocity` type we have to do: - -```cpp -struct velocity : derived_dimension, exp> {}; -``` - -In order to make `derived_dimension` work as expected it has to provide unique ordering for -contained base dimensions. Beside providing ordering to base dimensions it also has to: -- aggregate two arguments of the same base dimension but different exponents -- eliminate two arguments of the same base dimension and with opposite equal exponents - -`derived_dimension` is also able to form a dimension type based not only on base dimensions but -it can take other derived dimensions as well. In such a case units defined with a -`coherent_derived_unit` and `deduced_derived_unit` helper will get symbols of units for those -derived dimension (if those are named units) rather than system base units. - -For example to form `pressure` user can provide the following two definitions: - -```cpp -struct pressure : derived_dimension, exp, exp> {}; -``` - -or - -```cpp -struct pressure : derived_dimension, exp> {}; -``` - -In the second case `derived_dimension` will extract all derived dimensions into the list of -exponents of base dimensions. Thanks to this both cases will result with exactly the same base -class formed only from the exponents of base units but in the second case the recipe to form -a derived dimension will be remembered and used for `deduced_derived_unit` usage. - -#### `merge_dimension` - -`units::merge_dimension` is a type alias that works similarly to `derived_dimension` but instead -of sorting the whole list of base dimensions from scratch it assumes that provided input `dimension` -types are already sorted as a result of `derived_dimension`. Also contrary to `derived_dimension` -it works only with exponents of bas dimensions (no derived dimensions allowed). - -Typical use case for `merge_dimension` is to produce final `dimension` return type of multiplying -two different dimensions: - -```cpp -template -struct dimension_multiply; - -template -struct dimension_multiply, dimension> { - using type = downcast_traits_t, dimension>>; -}; - -template -using dimension_multiply = dimension_multiply::type; -``` - - -### `Units` - -`units::unit` is a class template that expresses the unit of a specific physical dimension: - -```cpp -template - requires (R::num * R::den > 0) -struct unit : downcast_base> { - using dimension = D; - using ratio = R; -}; -``` - -`units::Unit` is a concept that is satisfied by a type that is empty and publicly -derived from `units::unit` class template: - -```cpp -template -concept Unit = - std::is_empty_v && - detail::is_unit>; // exposition only -``` - -The library provides a bunch of helpers to create the derived unit: -- `named_coherent_derived_unit` -- `coherent_derived_unit` -- `named_scaled_derived_unit` -- `named_deduced_derived_unit` -- `deduced_derived_unit` -- `prefixed_derived_unit` - -Coherent derived units (units with `ratio<1>`) are created with a `named_coherent_derived_unit` -or `coherent_derived_unit` class templates: - -```cpp -template -struct named_coherent_derived_unit : downcast_child>> { - static constexpr bool is_named = true; - static constexpr auto symbol = Symbol; - using prefix_type = PT; -}; - -template -struct coherent_derived_unit : downcast_child>> { - static constexpr bool is_named = false; - static constexpr auto symbol = /* ... */; - using prefix_type = no_prefix; -}; -``` - -The above exposes public `prefix_type` member type and `symbol` used to print unit symbol -names. `prefix_type` is a tag type used to identify the type of prefixes to be used (i.e. SI, -data) and should satisfy the following concept: - -```cpp -template -concept PrefixType = std::derived_from; -``` - -For example to define the named coherent unit of `length`: - -```cpp -struct metre : named_coherent_derived_unit {}; -``` - -Again, similarly to `derived_dimension`, the first class template parameter is a CRTP idiom used -to provide downcasting facility (described below). - -`coherent_derived_unit` also provides a synthetized unit symbol. If all coherent units of -the recipe provided in a `derived_dimension` are named than the recipe is used to built a -unit symbol. Otherwise, the symbol will be created based on ingredient base units. - -```cpp -struct surface_tension : derived_dimension, exp> {}; -struct newton_per_metre : coherent_derived_unit {}; -``` - -To create scaled unit the following template should be used: - -```cpp -template -struct named_scaled_derived_unit : downcast_child> { - static constexpr bool is_named = true; - static constexpr auto symbol = Symbol; - using prefix_type = PT; -}; -``` - -For example to define `minute`: - -```cpp -struct minute : named_scaled_derived_unit> {}; -``` - -The `mp-units` library provides also a helper class templates to simplify the above process. -For example to create a prefixed unit the following may be used: - -```cpp -template - requires (!std::same_as) -struct prefixed_derived_unit : downcast_child>> { - static constexpr bool is_named = true; - static constexpr auto symbol = P::symbol + U::symbol; - using prefix_type = P::prefix_type; -}; -``` - -where `Prefix` is a concept requiring the instantiation of the following class template: - -```cpp -template -struct prefix : downcast_child> { - static constexpr auto symbol = Symbol; -}; -``` - -With this to create prefixed units user does not have to specify numeric value of the prefix ratio -or its symbol and just has to do the following: - -```cpp -struct kilometre : prefixed_derived_unit {}; -``` - -SI prefixes are predefined in the library and the user may easily provide his/her own with: - -```cpp -struct data_prefix : units::prefix_type {}; - -struct kibi : units::prefix, "Ki"> {}; -struct mebi : units::prefix, "Mi"> {}; -``` - -For the cases where determining the exact ratio is not trivial another helper can be used: - -```cpp -template -struct named_deduced_derived_unit : downcast_child { - static constexpr bool is_named = true; - static constexpr auto symbol = Symbol; - using prefix_type = PT; -}; - -template - requires U::is_named && (Us::is_named && ... && true) -struct deduced_derived_unit : downcast_child { - static constexpr bool is_named = false; - static constexpr auto symbol = /* even more magic to get the correct unit symbol */; - using prefix_type = no_prefix; -}; -``` - -This will deduce the ratio based on the ingredient units and their relation defined in the -dimension. For example to create a deduced velocity unit a user can do: - -```cpp -struct kilometre_per_hour : deduced_derived_unit {}; -``` - -`deduced_derived_unit` has also one more important feature. It is able to synthesize a unit -symbol: -- in case all units on the list are the units of base dimension (i.e. above we have a `kilometre` - that is a unit of a base dimension `length`, and `hour` a unit of base dimension `time`), - the resulting unit symbol will be created using base dimensions (`km/h`). -- if at least one non-base dimension unit exists in a list than the recipe provided in the - `derived_dimension` will be used to create a unit. For example: - - ```cpp - struct surface_tension : derived_dimension, exp> {}; - struct newton_per_centimetre : deduced_derived_unit {}; - ``` - - will result with a symbol `N/cm` for `newton_per_centimetre`. - - -### `Quantities` - -`units::quantity` is a class template that expresses the quantity/amount of a specific dimension +`quantity` is a class template that expresses the quantity/amount of a specific dimension expressed in a specific unit of that dimension: ```cpp -template -class quantity; +template U, Scalar Rep = double> +class quantity ``` -`units::Quantity` is a concept that is satisfied by a type that is an instantiation of -`units::quantity` class template: +`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 -concept Quantity = - detail::is_quantity; // exposition only -``` - -`units::quantity` provides the interface really similar to `std::chrono::duration`. The difference is that -it uses `double` as a default representation and has a few additional member types and functions as below: - -```cpp -template +template U, Scalar Rep = double> class quantity { public: + using dimension = D; using unit = U; using rep = Rep; - using dimension = U::dimension; - - [[nodiscard]] static constexpr quantity one() noexcept { return quantity(quantity_values::one()); } - - template - requires std::same_as> - [[nodiscard]] constexpr Scalar operator*(const quantity& lhs, - const quantity& rhs); - - template - requires (!std::same_as>) && - (treat_as_floating_point || - (std::ratio_multiply::den == 1)) - [[nodiscard]] constexpr Quantity operator*(const quantity& lhs, - const quantity& rhs); - - template - [[nodiscard]] constexpr Quantity operator/(const Rep1& v, - const quantity& q); - - template - requires std::same_as - [[nodiscard]] constexpr Scalar operator/(const quantity& lhs, - const quantity& rhs); - - template - requires (!std::same_as) && - (treat_as_floating_point || - (ratio_divide::den == 1)) - [[nodiscard]] constexpr Quantity operator/(const quantity& lhs, - const quantity& rhs); + [[nodiscard]] static constexpr quantity one() noexcept; // ... }; + +template + requires detail::basic_arithmetic && equivalent_dim> +[[nodiscard]] constexpr Scalar auto operator*(const quantity& lhs, + const quantity& rhs); + +template + requires detail::basic_arithmetic && (!equivalent_dim>) +[[nodiscard]] constexpr Quantity auto operator*(const quantity& lhs, + const quantity& rhs); + +template + requires std::magma +[[nodiscard]] constexpr Quantity auto operator/(const Value& v, + const quantity& q); + +template + requires detail::basic_arithmetic && equivalent_dim +[[nodiscard]] constexpr Scalar auto operator/(const quantity& lhs, + const quantity& rhs); + +template + requires detail::basic_arithmetic && (!equivalent_dim) +[[nodiscard]] constexpr Quantity AUTO operator/(const quantity& lhs, + const quantity& rhs); ``` -Additional functions provide the support for operations that result in a different dimension type -than those of their arguments. +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` to find a common representation - for a calculation result. Such a design was reported as problematic by numerics study group members - as sometimes we want to provide a different type in case of multiplication and different in case of - division. `std::common_type` lacks that additional information. That is why `units::quantity` uses - the resulting type of a concrete operator operation and provides it directly to `units::common_quantity_t` - type trait. + 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::si::length` for + `units::quantity`). -#### `quantity_cast` +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"" m(unsigned long long l) { return length(l); } +constexpr auto operator"" m(long double l) { return length(l); } + +// km +constexpr auto operator"" km(unsigned long long l) { return length(l); } +constexpr auto operator"" km(long double l) { return length(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 (`Unit`, `Rep`): +a `quantity` type or only its template parameters (`Dimension`, `Unit`, or `Rep`): ```cpp -template - requires same_dim -[[nodiscard]] constexpr To quantity_cast(const quantity& q); +template + requires QuantityOf && + detail::basic_arithmetic> +[[nodiscard]] constexpr auto quantity_cast(const quantity& q); -template -[[nodiscard]] constexpr quantity quantity_cast(const quantity& q); +template + requires equivalent_dim +[[nodiscard]] constexpr auto quantity_cast(const quantity& q); -template -[[nodiscard]] constexpr quantity quantity_cast(const quantity& q); +template + requires UnitOf +[[nodiscard]] constexpr auto quantity_cast(const quantity& q); -template -[[nodiscard]] constexpr quantity quantity_cast(const quantity& q); +template + requires detail::basic_arithmetic> +[[nodiscard]] constexpr auto quantity_cast(const quantity& q); ``` -#### `operator<<` +## Text output -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_derived_unit` class templates, the symbol provided +### 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 quantity has an unknown unit for a dimension predefined by the user with `derived_dimension`, - the symbol of a coherent unit of this dimension will be used. Additionally: - - if `Prefix` template parameter of a `coherent_derived_unit` is different than `no_prefix` then - the prefix symbol (i.e. `8 cJ`) defined by the specialization of `units::prefix_symbol` will be - aded wherever possible (`Ratio` matches the prefix ratio), +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 `PrefixType` 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. -3. If a quantity has an unknown dimension, the symbols of base dimensions will be used to construct - a unit symbol (i.e. `2 m/kg^2`). In this case no prefix symbols are added. + - 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`). -#### Text Formatting + +### `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 facility -formatters interpret the format specification as a `units-format-spec` according to the +[`fmt`](https://github.com/fmtlib/fmt) library). `parse()` member functions of +`fmt::formatter, CharT>` class template partial +specialization interprets the format specification as a `units-format-spec` according to the following syntax: ```text @@ -545,7 +536,7 @@ type: one of 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 +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. @@ -570,141 +561,95 @@ std::string s = fmt::format("{:=>12}", 120_kmph); // value of s is "====120 km/h ``` -## Strong types instead of aliases, and type downcasting facility +## 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 the important design decisions in the library are dictated by the requirement of +providing the best user experience as possible. -For example with template aliases usage the following code: +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 -const Velocity auto t = 20s; +using dim_capacitance = detail::derived_dimension_base, + exp, + exp, + exp>; ``` -could generate a following compile time error: +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 -\example\example.cpp:39:22: error: deduced initializer does not satisfy placeholder constraints - const Velocity auto t = 20s; - ^~~~ -In file included from \example\example.cpp:23: -/src/include/units/si/velocity.h:41:16: note: within 'template concept const bool units::Velocity [with T = units::quantity >, std::ratio<1> >, long long int>]' - concept Velocity = Quantity && std::same_as; - ^~~~~~~~ -In file included from /src/include/units/bits/tools.h:25, - from /src/include/units/dimension.h:25, - from /src/include/units/si/base_dimensions.h:25, - from /src/include/units/si/velocity.h:25, - from \example\example.cpp:23: -/src/include/units/bits/stdconcepts.h:33:18: note: within 'template concept const bool std::same_as [with T = units::dimension >; U = units::dimension,units::exp >]' - concept same_as = std::is_same_v; - ^~~~ -/src/include/units/bits/stdconcepts.h:33:18: note: 'std::is_same_v' evaluated to false +units::detail::derived_dimension_base, +units::exp, units::exp, +units::exp > ``` -Time and velocity are not that complicated dimensions and there are much more complicated dimensions -out there, but even for those dimensions +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`. -```text -[with T = units::quantity >, std::ratio<1> >, long long int>] -``` +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. -and - -```text -[with T = units::dimension >; U = units::dimension,units::exp >] -``` - -starts to be really hard to analyze or debug. - -That is why it was decided to provide automated downcasting capability when possible. Thanks to this feature the -same code will result with such an error: - -```text -\example\example.cpp:40:22: error: deduced initializer does not satisfy placeholder constraints - const Velocity t = 20s; - ^~~~ -In file included from \example\example.cpp:23: -/src/include/units/si/velocity.h:48:16: note: within 'template concept const bool units::Velocity [with T = units::quantity]' - concept Velocity = Quantity && std::same_as; - ^~~~~~~~ -In file included from /src/include/units/bits/tools.h:25, - from /src/include/units/dimension.h:25, - from /src/include/units/si/base_dimensions.h:25, - from /src/include/units/si/velocity.h:25, - from \example\example.cpp:23: -/src/include/units/bits/stdconcepts.h:33:18: note: within 'template concept const bool std::same_as [with T = units::time; U = units::velocity]' - concept same_as = std::is_same_v; - ^~~~ -/src/include/units/bits/stdconcepts.h:33:18: note: 'std::is_same_v' evaluated to false -``` - -Now - -```text -[with T = units::quantity] -``` - -and - -```text -[with T = units::time; U = units::velocity] -``` - -are not arguably much easier to understand thus provide better user experience. - -When dealing with simple types, aliases can be easily replaced with inheritance: +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](downcast_1.png) -As a result we get strong types. There are however a few issues with such an approach: -- generic code getting a child class does not easily know the exact template parameters of - the base class -- generic code after computing the instantiation of the class template does not know if - this is a base class in some hierarchy, and in case it is, it does not know how to - replace the base class template instantiation with a derived strong type. +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 provides such a type substitution mechanism. It connects a specific primary -template class instantiation with a strong type assigned to it by the user. +### Downcasting facility -Here is the overview of resulting class hierarchy for our example: +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](downcast_2.png) -In the above example `metre` is a downcasting target (child class) and a specific `unit` class -template instantiation is a downcasting source (base class). The downcasting facility provides -1 to 1 type substitution mechanism. Only one child class can be created for a specific base class -template instantiation. - -Downcasting facility is provided through 2 dedicated types, a concept, and a few helper template -aliases. +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 struct downcast_base { - using base_type = BaseType; + 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 `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. +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 concept Downcastable = requires { - typename T::base_type; + typename T::downcast_base_type; } && - std::derived_from>; + std::derived_from>; ``` -`units::Downcastable` is a concepts that verifies if a type implements and can be used in a downcasting -facility. +`units::Downcastable` is a concepts that verifies if a type implements and can be used in a +downcasting facility. ```cpp template @@ -715,27 +660,12 @@ struct downcast_child : T { `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. It is used in the following way to define `dimension` and -`unit` types in the library: - -```cpp -template -struct derived_dimension : downcast_child> {}; -``` - -```cpp -template -struct derived_unit : downcast_child>> {}; -``` +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. -```cpp -struct metre : named_derived_unit {}; -``` - 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: @@ -745,19 +675,9 @@ using downcast = decltype(detail::downcast_target_impl()); ``` `units::downcast` is used to obtain the target type of the downcasting operation registered -for a given instantiation in a base type. - -For example to determine a downcasted type of a quantity multiply operation the following can be done: - -```cpp -using dim = dimension_multiply; -using common_rep = decltype(lhs.count() * rhs.count()); -using ret = quantity>>, common_rep>; -``` - -`detail::downcast_target_impl` checks if a downcasting target is registered for the specific base class. -If yes, it returns the registered type, otherwise it works like a regular identity type returning -a provided base class. +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 { @@ -779,76 +699,159 @@ namespace detail { } ``` -Additionally there is on more simple helper alias provided that is used in the internal +Additionally there is one more simple helper alias provided that is used in the internal library implementation: ```cpp template -using downcast_base_t = T::base_type; +using downcast_base_t = T::downcast_base_type; ``` -## Adding custom dimensions and units +### `unknown_dimension` -In order to extend the library with custom dimensions the user has to: -1. Create a new base dimension if the predefined ones are not enough to form a new derived dimension: +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 = 123m; +units::Time auto t1 = 10s; +units::Velocity auto v1 = avg_speed(d1, t1); + +auto temp1 = v1 * 50m; // intermediate unknown dimension + +units::Velocity auto v2 = temp1 / 100m; // back to known dimensions again +units::Length auto d2 = v2 * 60s; +``` + +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_unit : unit {}; + +template +struct unknown_dimension : derived_dimension, + scaled_unit, unknown_unit>, + E, ERest...> { + using coherent_unit = scaled_unit, unknown_unit>; +}; +``` + +with this the error log or a debugger breakpoint involving a `temp1` type will include: + +```text +units::quantity, +units::exp >, units::unknown_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 - struct base_dim_digital_information : units::base_dimension<"digital information", "b"> {}; - ``` + namespace units::data { -2. Create a new dimension type with the recipe of how to construct it from base dimensions and - register it for a downcasting facility: + struct prefix : prefix_type {}; - ```cpp - struct digital_information : units::derived_dimension> {}; - ``` + struct kibi : units::prefix> {}; + struct mebi : units::prefix> {}; -3. Define a concept that will match a new dimension: - - ```cpp - template - concept DigitalInformation = units::QuantityOf; - ``` - -4. If non-SI prefixes should be applied to the unit symbol, define a new prefix tag and define - new prefixes using this tag and provide their ratio and symbol: - - ```cpp - struct data_prefix; - - struct kibi : units::prefix, "Ki"> {}; - struct mebi : units::prefix, "Mi"> {}; - ``` - -5. Define units and register them to a downcasting facility: - - ```cpp - struct bit : units::named_coherent_derived_unit {}; - struct kilobit : units::prefixed_derived_unit {}; - - struct byte : units::named_derived_unit> {}; - struct kilobyte : units::prefixed_derived_unit {}; - ``` - -6. Provide user-defined literals for the most important units: - - ```cpp - inline namespace literals { - constexpr auto operator""_b(unsigned long long l) { return units::quantity(l); } - constexpr auto operator""_b(long double l) { return units::quantity(l); } - - constexpr auto operator""_B(unsigned long long l) { return units::quantity(l); } - constexpr auto operator""_B(long double l) { return units::quantity(l); } } ``` -## Adding custom representations +2. New units for `information`: + + ```cpp + namespace units::data { + + struct bit : named_unit {}; + struct kibibit : prefixed_unit {}; + + struct byte : named_scaled_unit, bit> {}; + struct kibibyte : prefixed_unit {}; + + } + ``` + +3. New base dimension, its concept, and quantity alias: + + ```cpp + namespace units::data { + + struct dim_information : base_dimension<"information", bit> {}; + + template + concept Information = QuantityOf; + + template + using information = quantity; + + } + ``` + +4. UDLs for new units + + ```cpp + namespace units::data::inline literals { + + // bits + constexpr auto operator""b(unsigned long long l) { return information(l); } + constexpr auto operator""Kib(unsigned long long l) { return information(l); } + + // bytes + constexpr auto operator""B(unsigned long long l) { return information(l); } + constexpr auto operator""KiB(unsigned long long l) { return information(l); } + + } + ``` + +5. A new `bitrate` derived dimension, its units, concept, quantity helper, and UDLs + + ```cpp + namespace units::data { + + struct bit_per_second : unit {}; + struct dim_bitrate : derived_dimension, exp> {}; + + struct kibibit_per_second : deduced_unit {}; + + template + concept Bitrate = QuantityOf; + + template + using bitrate = quantity; + + inline namespace literals { + + // bits + constexpr auto operator""_bps(unsigned long long l) { return bitrate(l); } + constexpr auto operator""_Kibps(unsigned long long l) { return bitrate(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 following concept: +To provide basic library functionality the type should satisfy the `Scalar` concept: ```cpp template @@ -898,7 +901,6 @@ the equivalent operation for `Rep` type. Here is an additional list of opt-in op - `operator*=(Rep)` - `operator/=(Rep)` - `operator%=(Rep)` -- `operator%=(Rep)` - `operator%(Rep, Rep)` `quantity` also has 4 static functions `zero()`, `one()`, `min()`, and `max()` which can diff --git a/doc/design.png b/doc/design.png new file mode 100644 index 00000000..256bbd4a Binary files /dev/null and b/doc/design.png differ diff --git a/doc/downcast_1.png b/doc/downcast_1.png index 22768197..d8eaa468 100644 Binary files a/doc/downcast_1.png and b/doc/downcast_1.png differ diff --git a/doc/downcast_2.png b/doc/downcast_2.png index 7dbfc6c2..8a37faab 100644 Binary files a/doc/downcast_2.png and b/doc/downcast_2.png differ diff --git a/doc/nomnoml.md b/doc/nomnoml.md new file mode 100644 index 00000000..878d4ac9 --- /dev/null +++ b/doc/nomnoml.md @@ -0,0 +1,46 @@ +# nomnoml + +Graphs in the documentation are created with . + +## Concepts + +```text +[Dimension| +[base_dimension]<-[exp] +[derived_dimension]<-[exp] +[exp]<-[derived_dimension] +] + +[Quantity| +[quantity] +] + +[Unit]<-[Dimension] +[Dimension]<-[Quantity] +[Unit]<-[Quantity] +``` + +## Units + +```text +#direction: right + +[scaled_unit]<:-[unit] +[scaled_unit]<:-[named_unit] +[scaled_unit]<:-[named_scaled_unit] +[scaled_unit]<:-[prefixed_unit] +[scaled_unit]<:-[deduced_unit] +``` + +## Downcasting 1 + +```text +[detail::derived_dimension_base>]<:-[dim_area] +``` + +## Downcasting 2 + +```text +[downcast_base>>]<:-[detail::derived_dimension_base>] +[detail::derived_dimension_base>]<:-[downcast_child>>] +[downcast_child>>]<:-[dim_area]``` \ No newline at end of file diff --git a/doc/units.png b/doc/units.png new file mode 100644 index 00000000..311abd6f Binary files /dev/null and b/doc/units.png differ diff --git a/doc/units_uml.png b/doc/units_uml.png deleted file mode 100644 index f0f1e262..00000000 Binary files a/doc/units_uml.png and /dev/null differ