34 KiB
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:
// 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
- Safety and performance
- strong types
- compile-time safety
constexpr
all the things- as fast or even faster than when working with fundamental types
- The best possible user experience
- compiler errors
- debugging
- No macros in the user interface
- Easy extensibility
- No external dependencies
- Possibility to be standardized as a freestanding part of the C++ Standard Library
Basic Concepts
Below UML diagram shows the most important entities in the library design and how they relate to each other:
Dimensions
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:
template<Exponent... Es>
struct dimension : downcast_base<dimension<Es...>> {};
units::Dimension
is a concept that is satisfied by a type that is empty and publicly
derived from units::dimension
class template:
template<typename T>
concept Dimension =
std::is_empty_v<T> &&
detail::is_dimension<downcast_base_t<T>>; // exposition only
Exponents
units::Exponent
concept is satisfied if provided type is an instantiation of units::exp
class
template:
template<typename T>
concept Exponent =
detail::is_exp<T>; // exposition only
units::exp
provides an information about a single dimension and its (possibly fractional)
exponent in a derived dimension.
template<typename Dim, int Num, int Den = 1>
requires BaseDimension<Dim> || Dimension<Dim>
struct exp {
using dimension = Dim;
static constexpr int num = Num;
static constexpr int den = Den;
};
Both a base dimension and a derived dimension can be provided to units::exp
class template.
units::base_dimension
represents a base dimension and has assigned a unique compile-time text
describing the dimension name:
template<typename Name, typename Symbol>
struct base_dimension {
using name = Name;
using symbol = Symbol;
};
units::BaseDimension
is a concept to match all types derived from base_dimension
instantiations:
template<typename T>
concept BaseDimension = std::is_empty_v<T> &&
requires {
typename T::name;
typename T::symbol;
} &&
std::derived_from<T, base_dimension<typename T::name, typename T::symbol>>;
For example here is a list of SI base dimensions:
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.
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:
constexpr Velocity auto v1 = 1_m / 1s;
constexpr Velocity auto v2 = 2 / 2s * 1m;
static_assert(std::same_as<decltype(v1), decltype(v2)>);
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:
template<typename Child, Exponent... Es>
struct derived_dimension : downcast_child<Child, typename detail::make_dimension<Es...>::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:
struct velocity : derived_dimension<velocity, exp<length, 1>, exp<time, -1>> {};
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:
struct pressure : derived_dimension<pressure, exp<base_dim_mass, 1>, exp<base_dim_length, -1>, exp<base_dim_time, -2>> {};
or
struct pressure : derived_dimension<pressure, exp<force, 1>, exp<area, -1>> {};
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:
template<Dimension D1, Dimension D2>
struct dimension_multiply;
template<Exponent... E1, Exponent... E2>
struct dimension_multiply<dimension<E1...>, dimension<E2...>> {
using type = downcast_traits_t<merge_dimension_t<dimension<E1...>, dimension<E2...>>>;
};
template<Dimension D1, Dimension D2>
using dimension_multiply = dimension_multiply<typename D1::base_type, typename D2::base_type>::type;
Units
units::unit
is a class template that expresses the unit of a specific physical dimension:
template<Dimension D, Ratio R>
requires (R::num * R::den > 0)
struct unit : downcast_base<unit<D, R>> {
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:
template<typename T>
concept Unit =
std::is_empty_v<T> &&
detail::is_unit<downcast_base_t<T>>; // exposition only
The library provides a bunch of helpers to create the derived unit:
named_coherent_derived_unit<Child, Dim, Symbol, PrefixType>
coherent_derived_unit<Child, Dim>
named_scaled_derived_unit<Child, Dim, Symbol, R, PrefixType = no_prefix>
named_deduced_derived_unit<Child, Dim, Symbol, PrefixType, U, Us...>
deduced_derived_unit<Child, Dim, U, Us..>
prefixed_derived_unit<Child, P, U>
Coherent derived units (units with ratio<1>
) are created with a named_coherent_derived_unit
or coherent_derived_unit
class templates:
template<typename Child, Dimension Dim, basic_fixed_string Symbol, PrefixType PT>
struct named_coherent_derived_unit : downcast_child<Child, unit<Dim, ratio<1>>> {
static constexpr bool is_named = true;
static constexpr auto symbol = Symbol;
using prefix_type = PT;
};
template<typename Child, Dimension Dim>
struct coherent_derived_unit : downcast_child<Child, unit<Dim, ratio<1>>> {
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:
template<typename T>
concept PrefixType = std::derived_from<T, prefix_type>;
For example to define the named coherent unit of length
:
struct metre : named_coherent_derived_unit<metre, length, "m", si_prefix> {};
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.
struct surface_tension : derived_dimension<surface_tension, exp<force, 1>, exp<length, -1>> {};
struct newton_per_metre : coherent_derived_unit<newton_per_metre, surface_tension> {};
To create scaled unit the following template should be used:
template<typename Child, Dimension Dim, basic_fixed_string Symbol, Ratio R, PrefixType PT = no_prefix>
struct named_scaled_derived_unit : downcast_child<Child, unit<Dim, R>> {
static constexpr bool is_named = true;
static constexpr auto symbol = Symbol;
using prefix_type = PT;
};
For example to define minute
:
struct minute : named_scaled_derived_unit<minute, time, "min", ratio<60>> {};
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:
template<typename Child, Prefix P, Unit U>
requires (!std::same_as<typename U::prefix_type, no_prefix>)
struct prefixed_derived_unit : downcast_child<Child, unit<typename U::dimension,
ratio_multiply<typename P::ratio,
typename U::ratio>>> {
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:
template<typename Child, PrefixType PT, Ratio R, basic_fixed_string Symbol>
struct prefix : downcast_child<Child, detail::prefix_base<PT, R>> {
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:
struct kilometre : prefixed_derived_unit<kilometre, kilo, metre> {};
SI prefixes are predefined in the library and the user may easily provide his/her own with:
struct data_prefix : units::prefix_type {};
struct kibi : units::prefix<kibi, data_prefix, units::ratio< 1'024>, "Ki"> {};
struct mebi : units::prefix<mebi, data_prefix, units::ratio<1'048'576>, "Mi"> {};
For the cases where determining the exact ratio is not trivial another helper can be used:
template<typename Child, Dimension Dim, basic_fixed_string Symbol, PrefixType PT, Unit U, Unit... Us>
struct named_deduced_derived_unit : downcast_child<Child, /* magic to get the correct type */> {
static constexpr bool is_named = true;
static constexpr auto symbol = Symbol;
using prefix_type = PT;
};
template<typename Child, Dimension Dim, Unit U, Unit... Us>
requires U::is_named && (Us::is_named && ... && true)
struct deduced_derived_unit : downcast_child<Child, /* magic to get the correct type */> {
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:
struct kilometre_per_hour : deduced_derived_unit<kilometre_per_hour, velocity, kilometre, hour> {};
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 dimensionlength
, andhour
a unit of base dimensiontime
), 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:struct surface_tension : derived_dimension<surface_tension, exp<force, 1>, exp<length, -1>> {}; struct newton_per_centimetre : deduced_derived_unit<newton_per_centimetre, surface_tension, newton, centimetre> {};
will result with a symbol
N/cm
fornewton_per_centimetre
.
Quantities
units::quantity
is a class template that expresses the quantity/amount of a specific dimension
expressed in a specific unit of that dimension:
template<Unit 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:
template<typename T>
concept Quantity =
detail::is_quantity<T>; // 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:
template<Unit U, Scalar Rep = double>
class quantity {
public:
using unit = U;
using rep = Rep;
using dimension = U::dimension;
[[nodiscard]] static constexpr quantity one() noexcept { return quantity(quantity_values<Rep>::one()); }
template<Unit U1, Scalar Rep1, Unit U2, Scalar Rep2>
requires std::same_as<typename U1::dimension, dim_invert<typename U2::dimension>>
[[nodiscard]] constexpr Scalar operator*(const quantity<U1, Rep1>& lhs,
const quantity<U2, Rep2>& rhs);
template<Unit U1, Scalar Rep1, Unit U2, Scalar Rep2>
requires (!std::same_as<typename U1::dimension, dim_invert<typename U2::dimension>>) &&
(treat_as_floating_point<decltype(lhs.count() * rhs.count())> ||
(std::ratio_multiply<typename U1::ratio, typename U2::ratio>::den == 1))
[[nodiscard]] constexpr Quantity operator*(const quantity<U1, Rep1>& lhs,
const quantity<U2, Rep2>& rhs);
template<Scalar Rep1, typename U, typename Rep2>
[[nodiscard]] constexpr Quantity operator/(const Rep1& v,
const quantity<U, Rep2>& q);
template<Unit U1, Scalar Rep1, Unit U2, Scalar Rep2>
requires std::same_as<typename U1::dimension, typename U2::dimension>
[[nodiscard]] constexpr Scalar operator/(const quantity<U1, Rep1>& lhs,
const quantity<U2, Rep2>& rhs);
template<Unit U1, Scalar Rep1, Unit U2, Scalar Rep2>
requires (!std::same_as<typename U1::dimension, typename U2::dimension>) &&
(treat_as_floating_point<decltype(lhs.count() / rhs.count())> ||
(ratio_divide<typename U1::ratio, typename U2::ratio>::den == 1))
[[nodiscard]] constexpr Quantity operator/(const quantity<U1, Rep1>& lhs,
const quantity<U2, Rep2>& rhs);
// ...
};
Additional functions provide the support for operations that result in a different dimension type than those of their arguments.
Beside adding new elements a few other changes where applied compared to the std::chrono::duration
class:
- The
duration
is usingstd::common_type_t<Rep1, Rep2>
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 whyunits::quantity
uses the resulting type of a concrete operator operation and provides it directly tounits::common_quantity_t
type trait. operator %
is constrained withtreat_as_floating_point
type trait to limit the types to integral representations only. Alsooperator %(Rep)
takesRep
as a template argument to limit implicit conversions.
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
):
template<Quantity To, typename U, typename Rep>
requires same_dim<typename To::dimension, typename U::dimension>
[[nodiscard]] constexpr To quantity_cast(const quantity<U, Rep>& q);
template<Unit ToU, Scalar ToRep, typename U, typename Rep>
[[nodiscard]] constexpr quantity<ToU, ToRep> quantity_cast(const quantity<U, Rep>& q);
template<Unit ToU, typename U, typename Rep>
[[nodiscard]] constexpr quantity<ToU, Rep> quantity_cast(const quantity<U, Rep>& q);
template<Scalar ToRep, typename U, typename Rep>
[[nodiscard]] constexpr quantity<U, ToRep> quantity_cast(const quantity<U, Rep>& q);
operator<<
The library tries its best to print a correct unit of the quantity. This is why it performs a series of checks:
- If the user predefined a unit with a
named_XXX_derived_unit
class templates, the symbol provided by the user will be used (i.e.60 W
). - 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 acoherent_derived_unit
is different thanno_prefix
then the prefix symbol (i.e.8 cJ
) defined by the specialization ofunits::prefix_symbol
will be aded wherever possible (Ratio
matches the prefix ratio), - otherwise, non-standard ratio (i.e.
2 [60]Hz
) will be printed.
- if
- 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.
Text Formatting
mp-units
supports new C++20 formatting facility (currently provided as a dependency on
fmt
library). parse
member functions of facility
formatters interpret the format specification as a units-format-spec
according to the
following syntax:
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 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.
std::string s = fmt::format("{:=>12}", 120_kmph); // value of s is "====120 km/h"
Strong types instead of aliases, and type downcasting facility
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:
const Velocity auto t = 20s;
could generate a following compile time error:
<path>\example\example.cpp:39:22: error: deduced initializer does not satisfy placeholder constraints
const Velocity auto t = 20s;
^~~~
In file included from <path>\example\example.cpp:23:
<path>/src/include/units/si/velocity.h:41:16: note: within 'template<class T> concept const bool units::Velocity<T> [with T = units::quantity<units::unit<units::dimension<units::exp<units::base_dim_time, 1> >, std::ratio<1> >, long long int>]'
concept Velocity = Quantity<T> && std::same_as<typename T::dimension, velocity>;
^~~~~~~~
In file included from <path>/src/include/units/bits/tools.h:25,
from <path>/src/include/units/dimension.h:25,
from <path>/src/include/units/si/base_dimensions.h:25,
from <path>/src/include/units/si/velocity.h:25,
from <path>\example\example.cpp:23:
<path>/src/include/units/bits/stdconcepts.h:33:18: note: within 'template<class T, class U> concept const bool std::same_as<T, U> [with T = units::dimension<units::exp<units::base_dim_time, 1> >; U = units::dimension<units::exp<units::base_dim_length, 1>,units::exp<units::base_dim_time, -1> >]'
concept same_as = std::is_same_v<T, U>;
^~~~
<path>/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<units::dimension<units::exp<units::base_dim_time, 1> >, std::ratio<1> >, long long int>]
and
[with T = units::dimension<units::exp<units::base_dim_time, 1> >; U = units::dimension<units::exp<units::base_dim_length, 1>,units::exp<units::base_dim_time, -1> >]
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:
<path>\example\example.cpp:40:22: error: deduced initializer does not satisfy placeholder constraints
const Velocity t = 20s;
^~~~
In file included from <path>\example\example.cpp:23:
<path>/src/include/units/si/velocity.h:48:16: note: within 'template<class T> concept const bool units::Velocity<T> [with T = units::quantity<units::second, long long int>]'
concept Velocity = Quantity<T> && std::same_as<typename T::dimension, velocity>;
^~~~~~~~
In file included from <path>/src/include/units/bits/tools.h:25,
from <path>/src/include/units/dimension.h:25,
from <path>/src/include/units/si/base_dimensions.h:25,
from <path>/src/include/units/si/velocity.h:25,
from <path>\example\example.cpp:23:
<path>/src/include/units/bits/stdconcepts.h:33:18: note: within 'template<class T, class U> concept const bool std::same_as<T, U> [with T = units::time; U = units::velocity]'
concept same_as = std::is_same_v<T, U>;
^~~~
<path>/src/include/units/bits/stdconcepts.h:33:18: note: 'std::is_same_v' evaluated to false
Now
[with T = units::quantity<units::second, long long int>]
and
[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:
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.
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.
Here is the overview of resulting class hierarchy for our example:
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.
template<typename BaseType>
struct downcast_base {
using 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.
template<typename T>
concept Downcastable =
requires {
typename T::base_type;
} &&
std::derived_from<T, downcast_base<typename T::base_type>>;
units::Downcastable
is a concepts that verifies if a type implements and can be used in a downcasting
facility.
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. It is used in the following way to define dimension
and
unit
types in the library:
template<typename Child, Exponent... Es>
struct derived_dimension : downcast_child<Child, detail::make_dimension_t<Es...>> {};
template<typename Child, fixed_string Symbol, Dimension D>
struct derived_unit<Child, Symbol, D, R> : downcast_child<Child, unit<D, ratio<1>>> {};
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.
struct metre : named_derived_unit<metre, "m", length> {};
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:
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.
For example to determine a downcasted type of a quantity multiply operation the following can be done:
using dim = dimension_multiply<typename U1::dimension, typename U2::dimension>;
using common_rep = decltype(lhs.count() * rhs.count());
using ret = quantity<downcast<unit<dim, ratio_multiply<typename U1::ratio, typename U2::ratio>>>, 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.
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 on more simple helper alias provided that is used in the internal library implementation:
template<Downcastable T>
using downcast_base_t = T::base_type;
Adding custom dimensions and units
In order to extend the library with custom dimensions the user has to:
-
Create a new base dimension if the predefined ones are not enough to form a new derived dimension:
struct base_dim_digital_information : units::base_dimension<"digital information", "b"> {};
-
Create a new dimension type with the recipe of how to construct it from base dimensions and register it for a downcasting facility:
struct digital_information : units::derived_dimension<digital_information, units::exp<base_dim_digital_information, 1>> {};
-
Define a concept that will match a new dimension:
template<typename T> concept DigitalInformation = units::QuantityOf<T, digital_information>;
-
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:
struct data_prefix; struct kibi : units::prefix<kibi, data_prefix, units::ratio< 1'024>, "Ki"> {}; struct mebi : units::prefix<mebi, data_prefix, units::ratio<1'048'576>, "Mi"> {};
-
Define units and register them to a downcasting facility:
struct bit : units::named_coherent_derived_unit<bit, "b", digital_information, data_prefix> {}; struct kilobit : units::prefixed_derived_unit<kilobit, kibi, bit> {}; struct byte : units::named_derived_unit<byte, "B", digital_information, units::ratio<8>> {}; struct kilobyte : units::prefixed_derived_unit<kilobyte, kibi, byte> {};
-
Provide user-defined literals for the most important units:
inline namespace literals { constexpr auto operator""_b(unsigned long long l) { return units::quantity<bit, std::int64_t>(l); } constexpr auto operator""_b(long double l) { return units::quantity<bit, long double>(l); } constexpr auto operator""_B(unsigned long long l) { return units::quantity<byte, std::int64_t>(l); } constexpr auto operator""_B(long double l) { return units::quantity<byte, long double>(l); } }
Adding 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:
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>) &&
std::regular<T> &&
std::totally_ordered<T> &&
basic-arithmetic<T>;
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:
template<typename Rep>
inline constexpr bool treat_as_floating_point;
An example of such a type can be found in measurement example.
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)
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:
template<Scalar Rep>
struct quantity_values;