From b508a75f6c0a8c75e9efc65514a37a4d971074c5 Mon Sep 17 00:00:00 2001 From: Mateusz Pusz Date: Sat, 17 Nov 2018 12:40:06 +0100 Subject: [PATCH] README updated with design description --- README.md | 413 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 410 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 75c58b54..3fb27e88 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,411 @@ -# `units` +# `units` - Physical Units Library for C++ -Physical Units library implementation for C++. It bases on std::chrono::duration and adds other -dimensions. +## Summary + +`Units` is a compile-time friendly Modern C++ library that provides support for converting units. +The basic idea and design heavily bases on `std::chrono::duration` extending it to work properly +with many dimensions. + +Here is a small example of possible conversions: + +```cpp +static_assert(1000 / 1_s == 1_kHz); +static_assert(1_h == 3600_s); +static_assert(1_km + 1_m == 1001_m); +static_assert(10_km / 5_km == 2); +static_assert(10_km / 2 == 5_km); +static_assert(1_km / 1_s == 1000_mps); +static_assert(2_kmph * 2_h == 4_km); +static_assert(2_km / 2_kmph == 1_h); +``` + + +## Basic Concepts + +### `Dimensions` + +`units::dimension` is a type-list like type that stores an ordered list of exponents of one +or more base dimensions: + +```cpp +template +struct dimension { + using base_type = dimension; +}; +``` + +`units::Dimension` is a Concept that is satisfied by a type that is empty and publicly +derived from `units::dimension` class template: + +```cpp +template +concept bool Dimension = + std::is_empty_v && + detail::is_dimension && + DerivedFrom; +``` + +#### `Exponents` + +`units::exp` provides an information about single base dimension and its exponent in a derived +dimension: + +```cpp +template +struct exp { + using dimension = BaseDimension; + static constexpr int value = Value; +}; +``` + +where `BaseDimension` for now is: + +```cpp +template +using dim_id = std::integral_constant; +``` + +but it is meant to be replaced with C++20 class constexpr values provided as non-type template +parameters (when feature will be available in a compiler) so that for example base dimension for +length will be expressed as `dimension>`. + +`units::Exponent` concept is satisfied if provided type is an instantiation of `units::exp` class +template: + +```cpp +template +concept bool Exponent = detail::is_exp; +``` + +#### `make_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 C++ programmer. Also types visible in 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 / 1_s; +constexpr Velocity auto v2 = 2 / 2_s * 1_m; + +static_assert(Same); +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. To achieve that +dimension class templates should never be instantiated manually but through a `make_dimension_t` +template metaprogramming factory function: + +```cpp +template +struct make_dimension { + using type = /* unspecified */; +}; + +template +using make_dimension_t = typename make_dimension::type; +``` + +So for example to create a `dimension_velocity` type we have to do: + +```cpp +struct dimension_velocity : make_dimension_t, exp> {}; +``` + +Also for example to return the result of multiplying two different dimensions we have to +create a final dimension type using: + +```cpp +template +struct dimension_multiply; + +template +struct dimension_multiply, dimension> { + using type = upcasting_traits_t>; +}; + +template +using dimension_multiply_t = typename dimension_multiply::type; +``` + +In order to make `make_dimension_t` 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 + +Additionally, it would be good if the final type produced by `make_dimension_t` would be easy to +understand by the user, so for example base dimensions could be sorted with decreasing order of +their exponents. That is why second sorting of a type list may be required, for example: + +```cpp +template +struct make_dimension { + using type = mp::type_list_sort_t, exp_dim_id_less>>, exp_greater_equal>; +}; +``` + +### `Units` + +`units::unit` is a class template that expresses the unit of a specific dimension: + +```cpp +template + requires (R::num > 0) +struct unit { + using base_type = unit; + 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 bool Unit = + std::is_empty_v && + detail::is_unit && + DerivedFrom; +``` + +### `Quantities` + +`units::quantity` is a class template that expresses the quantity/amount of a specific dimension +expressed in a specific unit of that dimension: + +```cpp +template + requires Same +class quantity; +``` + +`units::Unit` is a Concept that is satisfied by a type that is a specialization of `units::quantity` +class template: + +```cpp +template +concept bool Quantity = detail::is_quantity; +``` + +`units::quantity` provides the interface really similar to `std::chrono::duration` with additional +member types and functions as below: + +```cpp +template + requires Same +class quantity { +public: + using dimension = D; + using unit = U; + + template + requires treat_as_floating_point> || std::ratio_multiply::den == 1 + quantity, upcasting_traits_t, std::ratio_multiply>>, std::common_type_t> + constexpr operator*(const quantity& lhs, + const quantity& rhs); + + template + quantity, upcasting_traits_t, std::ratio>>, std::common_type_t> + constexpr operator/(const Rep1& v, + const quantity& q); + + template + requires treat_as_floating_point> || std::ratio_divide::den == 1 + quantity, upcasting_traits_t, std::ratio_divide>>, std::common_type_t> + constexpr 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. + +#### `quantity_cast` + +To explicitly force truncating conversions `quantity_cast` function is provided which is a direct +counterpart of `std::chrono::duration_cast`. + +## Strong types instead of aliases and type upcasting capability + +Most of the important design decisions in the library are dictated by the requirement of providing +the best user experience as possible. + +For example the following code: + +```cpp +const Velocity t = 20_s; +``` + +could generate a following compile time error: + +```text +C:\repos\units\example\example.cpp:39:22: error: deduced initializer does not satisfy placeholder constraints + const Velocity t = 20_s; + ^~~~ +In file included from C:\repos\units\example\example.cpp:23: +C:/repos/units/src/include/units/si/velocity.h:41:16: note: within 'template concept const bool units::Velocity [with T = units::quantity >, units::unit >, std::ratio<1> >, long long int>]' + concept bool Velocity = Quantity && Same; + ^~~~~~~~ +In file included from C:/repos/units/src/include/units/bits/tools.h:25, + from C:/repos/units/src/include/units/dimension.h:25, + from C:/repos/units/src/include/units/si/base_dimensions.h:25, + from C:/repos/units/src/include/units/si/velocity.h:25, + from C:\repos\units\example\example.cpp:23: +C:/repos/units/src/include/units/bits/stdconcepts.h:33:18: note: within 'template concept const bool mp::std_concepts::Same [with T = units::dimension >; U = units::dimension, units::exp >]' + concept bool Same = std::is_same_v; + ^~~~ +C:/repos/units/src/include/units/bits/stdconcepts.h:33:18: note: 'std::is_same_v' evaluated to false +``` + +Time and velocity are not that complicated dimensions and there are much more complicated dimensions +out there, but even for those dimensions `[with T = units::quantity >, units::unit >, std::ratio<1> >, long long int>]` +and `[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 upcasting capability when possible. With that the +same code will result with such an error: + +```text +C:\repos\units\example\example.cpp:40:22: error: deduced initializer does not satisfy placeholder constraints + const Velocity t = 20_s; + ^~~~ +In file included from C:\repos\units\example\example.cpp:23: +C:/repos/units/src/include/units/si/velocity.h:48:16: note: within 'template concept const bool units::Velocity [with T = units::quantity]' + concept bool Velocity = Quantity && Same; + ^~~~~~~~ +In file included from C:/repos/units/src/include/units/bits/tools.h:25, + from C:/repos/units/src/include/units/dimension.h:25, + from C:/repos/units/src/include/units/si/base_dimensions.h:25, + from C:/repos/units/src/include/units/si/velocity.h:25, + from C:\repos\units\example\example.cpp:23: +C:/repos/units/src/include/units/bits/stdconcepts.h:33:18: note: within 'template concept const bool mp::std_concepts::Same [with T = units::dimension_time; U = units::dimension_velocity]' + concept bool Same = std::is_same_v; + ^~~~ +C:/repos/units/src/include/units/bits/stdconcepts.h:33:18: note: 'std::is_same_v' evaluated to false +``` + +Now `[with T = units::quantity]` and +`[with T = units::dimension_time; U = units::dimension_velocity]` are not arguably much better +user experience. + +Upcasting capability is provided through dedicated `upcasting_traits` and by `base_type` member +type in `dimension` and `unit` class templates. + +```cpp +template +struct upcasting_traits : std::type_identity {}; + +template +using upcasting_traits_t = typename upcasting_traits::type; +``` + +```cpp +struct dimension_length : make_dimension_t> {}; + +template<> +struct upcasting_traits : + std::type_identity {}; +``` + +```cpp +struct kilometer : unit {}; + +template<> +struct upcasting_traits : + std::type_identity {}; +``` + + +## Adding new dimensions + +The user to extend the library with his/her own dimensions has to: +1. Create a new dimension type and provide upcasting trait for it: + +```cpp +struct dimension_velocity : make_dimension_t, exp> {}; +template<> struct upcasting_traits : std::type_identity {}; +``` + +2. Define the base unit (`std::ratio<1>`) and additional ones plus provide upcasting traits for them +via: + +```cpp +struct meter_per_second : unit> {}; +template<> struct upcasting_traits : std::type_identity {}; +``` + +3. Define a concept that will match a new dimension: + +```cpp +template +concept bool Velocity = Quantity && Same; +``` + +4. Provide user-defined literals for most important units: + +```cpp +namespace literals { + constexpr auto operator""_mps(unsigned long long l) { return velocity(l); } + constexpr auto operator""_mps(long double l) { return velocity(l); } +} +``` + + +## Adding new base dimensions + +For now base dimensions are defined in terms of `std::integral_constant` and the provided +values must be unique. For example: + +```cpp +struct base_dim_length : dim_id<0> {}; +struct base_dim_mass : dim_id<1> {}; +struct base_dim_time : dim_id<2> {}; +struct base_dim_electric_current : dim_id<3> {}; +struct base_dim_temperature : dim_id<4> {}; +struct base_dim_amount_of_substance : dim_id<5> {}; +struct base_dim_luminous_intensity : dim_id<6> {}; +``` + +However, as soon as C++20 class type values will be supported as non-type template parameters +base dimensions will be just a text values. For example: + +```cpp +inline constexpr base_dim base_dim_length = "length"; +``` + +With that it should be really easy to add support for any new non-standard base unit to the +library without the risk of collision with any dimension type defined by the library itself or +by other users extending the library with their own dimension types. + + +## Open questions + +1. Should we ensure that dimension is always a result of make_dimension? How to do it? + +2. Should we provide strong types and upcasting_traits for `quantity` type? + + In such a case all the operators have to be provided to a child class. Or maybe use CRTP? + +3. What to do with time which ia ambiguous? + +4. What to do with `std::chrono::duration`? + +5. What is the best way to add support for temperatures? + + Temperatures require not only require `std::ratio` but also should adjusted/shifted by some + constant values (i.e. [°C] = [K] − 273.15). + +6. Should the "base dimension" be better expressed/isolated by the design? + +7. `seconds` or `time`? + +8. How to use CTAD? + + CTAD for alias templates were already supported by EWG in San Diego 2018 so `length(3.5)` + will work. However,deduction with partial argument lists was rejected so `length(3)` + will not be supported for now.