Custom Represetnation Types chapter done

This commit is contained in:
Mateusz Pusz
2020-03-26 16:58:37 +01:00
parent ecb158eaa7
commit d8d3dcf339

View File

@@ -15,76 +15,250 @@ A `Scalar` concept
To support a minimum set of `quantity` operations all custom representation types have to
satisfy at least the `Scalar` concept. Which means that they:
- cannot be quantities or be wrappers over the `quantity` type
(i.e. ``std::optional<si::length<si::metre>>``),
- cannot be quantities by themselves,
- cannot be wrappers over the `quantity` type (i.e. ``std::optional<si::length<si::metre>>``),
- have to be regular types (e.g. they have to provide equality operators)
- must be constructible from a fundamental integral type (to
- if they are constructible from a fundamental integral type, they have to provide multiplication
and division operators for their types,
- otherwise, their values need to support the multiplication or division by the values of the
``std::int64_t`` type which is the type used to store elements of a `ratio`.
With the above we will be able to construct quantities, convert between the units of the same
dimension and compare them for equality. To provide additional `quantity` operations the
custom representation type have to satisfy more requirements.
dimension, and compare them for equality.
Additional requirements
-----------------------
The Simplest Custom Representation Type
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. important::
The simplest representation type that fullfills the above requirements can look as follows::
The requirements described in the chapter are optional in a meaning that if someone does
not plan to use a specific quantity's operation his/her custom representation type can
ignore (not implement/satisfy) the requirements for it.
class my_rep {
int value_{};
public:
my_rep() = default;
constexpr my_rep(int v) : value_(v) {}
[[nodiscard]] friend constexpr bool operator==(my_rep lhs, my_rep rhs) = default;
[[nodiscard]] friend constexpr my_rep operator*(my_rep lhs, my_rep rhs)
{
return my_rep(lhs.value_ * rhs.value_);
}
[[nodiscard]] friend constexpr my_rep operator/(my_rep lhs, my_rep rhs)
{
return my_rep(lhs.value_ / rhs.value_);
}
};
Now we can put ``my_rep`` as the last parameter of the `quantity` class template and the following
code will work just fine::
static_assert(si::length<si::metre, my_rep>(2'000) == si::length<si::kilometre, my_rep>(2));
Construction of Quantities with Custom Representation Types
-----------------------------------------------------------
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Let's assume two types:
Assuming that our custom representation type is constructible from a fundamental integral type,
let's assume two types:
.. code-block::
:emphasize-lines: 6, 15
:emphasize-lines: 5, 13
template<typename T>
class impl {
T value_{};
int value_{};
public:
impl() = default;
constexpr impl(T v): value_(v) {}
constexpr impl(int v): value_(v) {}
// the rest of the representation type implementation
};
template<typename T>
class expl {
T value_{};
int value_{};
public:
expl() = default;
constexpr explicit expl(T v): value_(v) {}
constexpr explicit expl(int v): value_(v) {}
// the rest of the representation type implementation
};
The difference between the above types is that ``impl`` class is implicitly constructible
from values of type ``T`` while ``expl`` is not. To create quantities using those types as
from values of type ``int`` while ``expl`` is not. To create quantities using those as their
representation types we have to obey similar rules::
si::length<si::metre, impl<int>> d1(123); // OK
si::length<si::metre, expl<int>> d2(123); // Compile-time error
si::length<si::metre, expl<int>> d3(expl(123)); // OK
si::length<si::metre, impl> d1(123); // OK
si::length<si::metre, expl> d2(123); // Compile-time error
si::length<si::metre, expl> d3(expl(123)); // OK
This also applies when we want to create a quantity with a custom representation type
from a regular quantity value::
Length auto d = 123q_m;
si::length<si::metre, impl<int>> d1(d); // OK
si::length<si::metre, expl<int>> d2(d); // Compile-time error
si::length<si::metre, expl<int>> d3(quantity_cast<expl<int>>(d)); // OK
si::length<si::metre, impl> d1(d); // OK
si::length<si::metre, expl> d2(d); // Compile-time error
si::length<si::metre, expl> d3(quantity_cast<expl>(d)); // OK
The only difference here is that in this case we have to explicitly cast the `quantity` with
`quantity_cast` overload that scopes only on changing the representation type.
Additional Requirements
-----------------------
As noted in the previous chapter, the `Scalar` concept guarantees us the possibility to
construct quantities, convert between the units of the same dimension, and compare them
for equality. To provide additional `quantity` operations the custom representation type
have to satisfy more requirements.
.. important::
The requirements described in this chapter are optional in a meaning that if someone does
not plan to use a specific quantity's operation his/her custom representation type can
ignore (not implement/satisfy) the requirements for it.
Relational Quantity Operators
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In case we want to to compare the values of `quantity` type not only for equality but
also for ordering, we have to provide a corresponding operators to our ``my_rep`` class.
With C++20 it is really easy to do::
class my_rep {
public:
[[nodiscard]] friend constexpr auto operator<=>(my_rep lhs, my_rep rhs) = default;
// ...
};
With the above the following code will compile fine::
static_assert(si::length<si::metre, my_rep>(2'000) < si::length<si::kilometre, my_rep>(3));
Arithmetic Quantity Operators
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
In case we plan to perform arithmetic operations on our `quantity` type we have to provide
at least the following::
class my_rep {
public:
[[nodiscard]] friend constexpr my_rep operator+(my_rep lhs, my_rep rhs)
{
return my_rep(lhs.value_ + rhs.value_);
}
[[nodiscard]] friend constexpr my_rep operator-(my_rep lhs, my_rep rhs)
{
return my_rep(lhs.value_ - rhs.value_);
}
// ...
};
Thanks to it the following code will run as expected::
static_assert(si::length<si::metre, my_rep>(2'000) + si::length<si::kilometre, my_rep>(1) ==
si::length<si::kilometre, my_rep>(3));
Of course, the above operators are the smallest possible set to provide support for basic
arithmetic operations. In case the user wants to use faster or more sofisticated operators
the following ones can be provided::
class my_rep {
public:
[[nodiscard]] constexpr my_rep operator+() const;
[[nodiscard]] constexpr my_rep operator-() const;
constexpr my_rep& operator++();
constexpr my_rep operator++(int);
constexpr my_rep& operator--();
constexpr my_rep operator--(int);
constexpr my_rep& operator+=(my_rep q);
constexpr my_rep& operator-=(my_rep q);
constexpr my_rep& operator*=(my_rep rhs);
constexpr my_rep& operator/=(my_rep rhs);
constexpr my_rep& operator%=(my_rep rhs);
[[nodiscard]] friend constexpr my_rep operator%(my_rep lhs, my_rep rhs);
// ...
};
Each of the above operators will enable a respective operator in the `quantity`
type.
Customization Points
--------------------
Up to now we were enabling new functionalities by adding new operations to the custom representation
type. However, we can also enable more operations and customize the engine behavior through a few
customization points.
`quantity_value`
^^^^^^^^^^^^^^^^
The `quantity` class template has a few static member functions: `quantity::zero`, `quantity::one`,
`quantity::min`, and `quantity::max`. Those return the respective quantity values for a specific
representation type. The default implementation is provided through the `quantity_values` class
template::
template<Scalar Rep>
struct quantity_values {
static constexpr Rep zero() noexcept { return Rep(0); }
static constexpr Rep one() noexcept { return Rep(1); }
static constexpr Rep min() noexcept { return std::numeric_limits<Rep>::lowest(); }
static constexpr Rep max() noexcept { return std::numeric_limits<Rep>::max(); }
};
The user can provide an explicit/partial class template specialization for his/her custom
representation type and provide values that should be returned by the respective `quantity`
operations.
`treat_as_floating_point`
^^^^^^^^^^^^^^^^^^^^^^^^^
In the :ref:`Conversions and Casting` chapter we learned that the conversions provided by the
library's framework treat floating-point representation types differently than the integral
ones. This behavior can also be extended to the custom representation types with
`treat_as_floating_point` customization point which default definition is::
template<Scalar Rep>
inline constexpr bool treat_as_floating_point = std::is_floating_point_v<Rep>;
If our representation type should have a floating-point semantics or if it is a class
template, in which case we may not know exactly what is the final representation type,
we can specialize this variable template as follows::
namespace custom {
template<typename T>
class my_rep {
T value_{};
public:
// ...
};
} // namespace custom
namespace units {
template<typename T>
inline constexpr bool treat_as_floating_point<custom::my_rep<T>> = std::is_floating_point_v<T>;
} // namespace units
.. important::
Please remember that by the C++ language rules all template specializations have to be put
always in the same namespace as the primary template definition.
Conversions of Quantities with Custom Representation Types
----------------------------------------------------------
In case we want to mix quantities of our Custom Representation Type with the quantities using
fundamental arithmetic types as their representation we have to provide conversion operators.
fundamental arithmetic types as their representation we have to provide conversion operators
in our representation type.
Again let's assume two types but this time let's scope on conversion operators rather
Again let's assume two types, but this time let's scope on conversion operators rather
than on constructors:
.. code-block::
@@ -114,7 +288,7 @@ If we have instances of the above types we can construct quantities in the follo
si::length<si::metre, int> d2(v_expl); // Compile-time error
si::length<si::metre, int> d3(int(v_expl); // OK
Similarly, when we have quantities of above types we can create quantities of other
Similarly, when we have quantities of the above types we can create quantities of other
representation types with::
si::length<si::metre, impl<int>> d_impl(1);
@@ -124,21 +298,7 @@ representation types with::
si::length<si::metre, int> d3(quantity_cast<int>(d_expl)); // OK
Tricky cases
------------
Customization points
--------------------
treat_as_floating_point
quantity_value
.. seealso::
For more examples of custom representation types usage please refer to :ref:`measurement`
example.
For more examples of custom representation types usage please refer to the
:ref:`Linear Algebra of Quantities` chapter and :ref:`measurement` example.