# `mp-units` - A Units Library for C++ ## Summary `Units` is a compile-time enabled Modern C++ library that provides compile-time dimensional analysis and unit/quantity manipulation. The basic idea and design heavily bases on `std::chrono::duration` and extends it to work properly with many dimensions. Here is a small example of possible operations: ```cpp // simple numeric operations static_assert(10km / 2 == 5km); // unit conversions static_assert(1h == 3600s); static_assert(1km + 1m == 1001m); // dimension conversions static_assert(1km / 1s == 1000mps); static_assert(2kmph * 2h == 4km); static_assert(2km / 2kmph == 1h); static_assert(1000 / 1s == 1kHz); static_assert(10km / 5km == 2); ``` ## Approach 1. Safety and performance - strong types - compile-time safety - `constexpr` all the things 2. The best possible user experience - compiler errors - debugging 3. No macros in the user interface 4. Easy extensibility 5. No external dependencies 6. Possibility to be standardized as a freestanding part of the C++ Standard Library ## Overview The library framework consists of a few concepts: quantities, units, dimensions and their exponents. From the user's point of view the most important is a quantity. Quantity is a concrete amount of a unit for a specified dimension with a specific representation: ```cpp units::quantity d1(123); auto d2 = 123km; // stde::units::quantity ``` There are C++ concepts provided for each such quantity type: ```cpp template concept Length = QuantityOf; ``` With that we can easily write a function template like this: ```cpp constexpr units::Velocity auto avg_speed(units::Length auto d, units::Time auto t) { return d / t; } ``` ## Basic Concepts ### `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 : downcast_base> {}; ``` `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 Dimension = std::is_empty_v && detail::is_dimension>; // exposition only ``` #### `Exponents` `units::exp` provides an information about a single base dimension and its (possibly fractional) exponent in a derived dimension: ```cpp template struct exp { static constexpr const base_dimension& dimension = BaseDimension; static constexpr int num = Num; static constexpr int den = Den; }; ``` where `BaseDimension` is a unique sortable compile-time value: ```cpp struct base_dimension { const char* name; }; constexpr bool operator==(const base_dimension& lhs, const base_dimension& rhs); constexpr bool operator<(const base_dimension& lhs, const base_dimension& rhs); ``` `units::Exponent` concept is satisfied if provided type is an instantiation of `units::exp` class template: ```cpp template concept Exponent = detail::is_exp; // exposition only ``` #### `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 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 `make_dimension_t` template metaprogramming factory function: ```cpp template struct make_dimension { using type = /* unspecified */; }; template using make_dimension_t = make_dimension::type; ``` So for example to create a `velocity` type we have to do: ```cpp struct velocity : make_dimension_t, exp> {}; ``` 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 #### `merge_dimension` `units::merge_dimension` is similar to `make_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 `make_dimension`. 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 = downcasting_traits_t, dimension>>; }; template using dimension_multiply_t = dimension_multiply::type; ``` Example implementation of `merge_dimension` may look like: ```cpp template struct merge_dimension { using type = detail::dim_consolidate_t>; }; ``` ### `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; }; ``` For example to define the base unit of `length`: ```cpp struct metre : unit {}; ``` Also there are few alias templates provided as convenience helpers to simplify `Ratio` handling: - units with prefixes ```cpp struct kilometre : kilo {}; ``` - derived units ```cpp struct kilometre_per_hour : derived_unit {}; ``` `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 ``` ### `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 class quantity; ``` `units::Quantity` is a Concept that is satisfied by a type that is an instantiation of `units::quantity` class template: ```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 class quantity { public: 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); // ... }; ``` Additional functions provide the support for operations that result in a different dimension type than those of their arguments. Another change comparing to `std::chrono::duration` is that 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. #### `quantity_cast` To explicitly force truncating conversions `quantity_cast` function is provided which is a direct counterpart of `std::chrono::duration_cast`. As a template argument user can provide here either a `quantity` type or only its template parameters (`Unit`, `Rep`): ```cpp template requires std::same_as constexpr To quantity_cast(const quantity& q); template constexpr quantity quantity_cast(const quantity& q); ``` ## Strong types instead of aliases, and type downcasting 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 with template aliases usage the following code: ```cpp const Velocity auto t = 20s; ``` could generate a following compile time error: ```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 stde::units::Velocity [with T = stde::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 = stde::units::dimension >; U = stde::units::dimension,stde::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 ``` Time and velocity are not that complicated dimensions and there are much more complicated dimensions out there, but even for those dimensions ```text [with T = stde::units::quantity >, std::ratio<1> >, long long int>] ``` and ```text [with T = stde::units::dimension >; U = stde::units::dimension,stde::units::exp >] ``` starts to be really hard to analyze or debug. That is why it was decided to provide automated downcasting capability when possible. With that 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 stde::units::Velocity [with T = stde::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 = stde::units::time; U = stde::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 = stde::units::quantity] ``` and ```text [with T = stde::units::time; U = stde::units::velocity] ``` are not arguably much easier to understand thus provide better user experience. Downcasting capability is provided through dedicated `downcasting_traits`, concept, a few helper aliases and by `base_type` member type in `downcast_base` class template. ```cpp template struct downcast_base { using base_type = BaseType; }; template concept bool Downcastable = requires { typename T::base_type; } && std::derived_from>; template using downcast_from = T::base_type; template using downcast_to = std::type_identity; template struct downcasting_traits : downcast_to {}; template using downcasting_traits_t = downcasting_traits::type; ``` With that the downcasting functionality is enabled by: ```cpp struct length : make_dimension_t> {}; template<> struct downcasting_traits> : downcast_to {}; ``` ```cpp struct kilometre : unit {}; template<> struct downcasting_traits> : downcast_to {}; ``` ## Adding custom dimensions and units In order to extend the library with custom dimensions the user has to: 1. Create a new base dimension if the predefined ones are not enough to form a new derived dimension: ```cpp inline constexpr units::base_dimension base_dim_digital_information{"digital information"}; ``` 2. Create a new dimension type with the recipe of how to construct it from base dimensions and provide downcasting trait for it: ```cpp struct digital_information : units::make_dimension_t> {}; template<> struct units::downcasting_traits> : units::downcast_to {}; ``` 2. Define a concept that will match a new dimension: ```cpp template concept DigitalInformation = units::QuantityOf; ``` 3. Define units and provide downcasting traits for them: ```cpp struct bit : units::unit {}; template<> struct units::downcasting_traits> : units::downcast_to {}; struct byte : units::unit> {}; template<> struct units::downcasting_traits> : units::downcast_to {}; ``` 4. 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); } } ``` ## Open questions 1. Should we ensure that dimension is always a result of `make_dimension`? How to do it? 2. What to do with `time` which is ambiguous (conflict wit ANSI C)? 3. What to do with `std::chrono::duration`? 4. Should we provide `seconds` or stay with `quantity`? 5. What is the best way to add support for temperatures? Temperature absolute values not only require `std::ratio` but also should be adjusted/shifted by some constant values (i.e. [°C] = [K] − 273.15). Relative temperatures does need an offset. Users will most probably have problems with differentiating those two. Maybe the best solution is to provide only `K` support in quantity and provide non-member helper conversion functions with verbose names to convert to `°C` and `°C`? 6. Do we need non-linear scale? 7. Should we provide cmath-like functions for quantities? 8. What should be the resulting type of `auto d = 1km + 1ft;`? 9. Should we require explicit casts (i.e. quantity_cast) between different systems of measurement? 10. Should we support integral representations? 11. Provide ostream overloads to print quantity units (use `std::format`)? 12. Should we provide support for dimensionless quantities? Because dimensionless quantities have no associated units, they behave as normal scalars, and allow implicit conversion to and from the underlying value type or types that are convertible to/from that value type. 13. Should we standardize accompany tools (`downcasting_traits`, `type_list` operations, `common_ratio`, etc)? 14. Do we need to support fractional exponents (i.e. `dimension>` as 2/3)? 15. `k`, `K`, `W`, `F` UDLs conflict with gcc GNU extensions (https://gcc.gnu.org/onlinedocs/gcc-4.3.0/gcc/Fixed_002dPoint.html) for floating point types. 16. `J` imaginary constants are a GCC extension