docs: "Interoperability with Other Libraries" chapter added

This commit is contained in:
Mateusz Pusz
2023-12-18 15:39:01 +01:00
parent f28ba96cb4
commit 590d1fab85
3 changed files with 308 additions and 1 deletions

View File

@@ -0,0 +1,307 @@
# Interoperability with Other Libraries
**mp-units** makes it easy to cooperate with similar entities of other libraries.
No matter if we want to provide interoperability with a simple home-grown strongly typed
wrapper type (e.g., `Meter`, `Timestamp`, ...) or with a feature-rich quantities and units
library, we have to provide specializations of:
- a `quantity_like_traits` for external `quantity`-like type,
- a `quantity_point_like_traits` for external `quantity_point`-like type.
## Specifying a conversion kind
Before we delve into the template specialization details, let's first decide if we want the
conversions to happen implicitly or if explicit ones would be a better choice. Or maybe
the conversion should be implicit in one direction only (e.g., into **mp-units** abstractions) while the explicit conversions in the other direction should be preferred?
There is no one unified answer to the above questions. Everything depends on the use case.
Typically, the implicit conversions are allowed in cases where:
- both abstractions mean exactly the same, and interchanging them in the code should not change
its logic,
- there is no significant runtime overhead introduced by such a conversion (e.g., no need for dynamic
allocation or copying of huge internal buffers),
- the target type of the conversion provides the same or better safety to the users,
- we prefer the simplicity of implicit conversions over safety during the (hopefully short)
transition period of refactoring our code base from the usage of one library to the other.
In all other scenarios, we should probably enforce explicit conversions.
The kinds of inter-library conversions can be easily configured in partial specializations
of conversion traits in the **mp-units** library. To require an explicit conversion, the return
type of the conversion function should be wrapped in `convert_explicitly<T>`. Otherwise,
`convert_implicitly<T>` should be used.
## Quantities conversions
For example, let's assume that some company has its own `Meter` strong-type wrapper:
```cpp
struct Meter {
int value;
};
```
As every usage of `Meter` is at least as good and safe as the usage of `quantity<si::metre, int>`,
and as there is no significant runtime performance penalty, we would like to allow the conversion
to `mp_units::quantity` to happen implicitly.
On the other hand, the `quantity` type is much safer than the `Meter`, and that is why we would prefer
to see the opposite conversions stated explicitly in our code.
To enable such interoperability, we must define a partial specialization of
the `quantity_like_traits<T>` type trait. Such specialization should provide:
- static data member `reference` that provides the quantity reference (e.g., unit),
- `rep` type that specifies the underlying storage type,
- `to_numerical_value(T)` static member function returning a quantity's raw value of `rep` type
packed in either `convert_explicitly` or `convert_implicitly` wrapper.
- `from_numerical_value(rep)` static member function returning `T` packed in either `convert_explicitly`
or `convert_implicitly` wrapper.
For example, for our `Meter` type, we could provide the following:
```cpp
template<>
struct mp_units::quantity_like_traits<Meter> {
static constexpr auto reference = si::metre;
using rep = decltype(Meter::value);
static constexpr convert_implicitly<rep> to_numerical_value(Meter m) { return m.value; }
static constexpr convert_explicitly<Meter> from_numerical_value(rep v) { return Meter{v}; }
};
```
After that, we can check that the [`QuantityLike`](../framework_basics/concepts.md#QuantityLike)
concept is satisfied:
```cpp
static_assert(mp_units::QuantityLike<Meter>);
```
and we can write the following:
```cpp
void print(Meter m) { std::cout << m.value << " m\n"; }
int main()
{
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
Meter height{42};
// implicit conversions
quantity h1 = height;
quantity<isq::height[m], int> h2 = height;
std::cout << h1 << "\n";
std::cout << h2 << "\n";
// explicit conversions
print(Meter(h1));
print(Meter(h2));
}
```
!!! note
No matter if we decide to use implicit or explicit conversions, the **mp-units** will not
allow unsafe operations to happen.
If we extend the above example with unsafe conversions, the code will not compile, and we will
have to fix the issues first before the conversion may be performed:
=== "Unsafe"
```cpp
quantity<isq::height[m]> h3 = height;
quantity<isq::height[mm], int> h4 = height;
quantity<isq::height[km], int> h5 = height; // Compile-time error (1)
std::cout << h3 << "\n";
std::cout << h4 << "\n";
std::cout << h5 << "\n";
print(Meter(h3)); // Compile-time error (2)
print(Meter(h4)); // Compile-time error (3)
print(Meter(h5));
```
1. Truncation of value while converting from meters to kilometers.
2. Conversion of `double` to `int` is not value-preserving.
3. Truncation of value while converting from millimeters to meters.
=== "Fixed"
```cpp
quantity<isq::height[m]> h3 = height;
quantity<isq::height[mm], int> h4 = height;
quantity<isq::height[km], int> h5 = quantity{height}.force_in(km);
std::cout << h3 << "\n";
std::cout << h4 << "\n";
std::cout << h5 << "\n";
print(Meter(value_cast<int>(h3)));
print(Meter(h4.force_in(m)));
print(Meter(h5));
```
## Quantity points conversions
To play with quantity point conversions, let's assume that we have a `Timestamp` strong type in our
codebase, and we would like to start using **mp-units** to work with this abstraction.
```cpp
struct Timestamp {
int seconds;
};
```
As we described in [The Affine Space](../framework_basics/the_affine_space.md) chapter, timestamps
should be modeled as quantity points rather than regular quantities.
To allow the conversion between our custom `Timestamp` type and the `quantity_point` class template
we need to provide the following in the partial specialization of the `quantity_point_like_traits<T>`
type trait:
- static data member `reference` that provides the quantity point reference (e.g., unit),
- static data member `point_origin` that specifies the absolute point, which is the beginning of
our measurement scale for our points,
- `rep` type that specifies the underlying storage type,
- `to_quantity(T)` static member function returning the `quantity` being the offset of the point
from the origin packed in either `convert_explicitly` or `convert_implicitly` wrapper,
- `from_quantity(quantity<reference, rep>)` static member function returning `T` packed in either
`convert_explicitly` or `convert_implicitly` wrapper.
For example, for our `Timestamp` type, we could provide the following:
```cpp
inline constexpr struct TimestampOrigin :
mp_units::absolute_point_origin<TimestampOrigin, mp_units::isq::time> {} TimestampOrigin;
template<>
struct mp_units::quantity_point_like_traits<Timestamp> {
static constexpr auto reference = si::second;
static constexpr auto point_origin = TimestampOrigin;
using rep = decltype(Timestamp::seconds);
static constexpr convert_implicitly<quantity<reference, rep>> to_quantity(Timestamp ts)
{
return ts.seconds * si::second;
}
static constexpr convert_explicitly<Timestamp> from_quantity(quantity<reference, rep> q)
{
return Timestamp(q.numerical_value_ref_in(si::second));
}
};
```
After that, we can check that the [`QuantityLike`](../framework_basics/concepts.md#QuantityLike)
concept is satisfied:
```cpp
static_assert(mp_units::QuantityPointLike<Timestamp>);
```
and we can write the following:
```cpp
void print(Timestamp ts) { std::cout << ts.seconds << " s\n"; }
int main()
{
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
Timestamp ts{42};
// implicit conversion
quantity_point qp = ts;
std::cout << qp.quantity_from(TimestampOrigin) << "\n";
// explicit conversion
print(Timestamp(qp));
}
```
## Interoperability with the C++ Standard Library
In the C++ standard library, we have two types that handle quantities and model the affine space.
Those are:
- [`std::chrono::duration`](https://en.cppreference.com/w/cpp/chrono/duration) - specifies
quantities of time,
- [`std::chrono::time_point`](https://en.cppreference.com/w/cpp/chrono/time_point) - specifies
quantity points of time.
The **mp-units** library comes with built-in interoperability with those types. It is enough to include the _mp-units/chrono.h_ file to benefit from it. This file provides:
- partial specializations of `quantity_like_traits` and `quantity_point_like_traits` that provide
support for implicit conversions between `std` and `mp_units` types in both directions,
- `chrono_point_origin<Clock>` point origin for `std` clocks,
- `to_chrono_duration` and `to_chrono_time_point` dedicated conversion functions that result
in types exactly representing **mp-units** abstractions.
!!! important
Only a `quantity_point` that uses `chrono_point_origin<Clock>` as its origin can be converted
to the `std::chrono` abstractions:
```cpp
inline constexpr struct ts_origin : relative_point_origin<chrono_point_origin<system_clock> + 1 * h> {} ts_origin;
inline constexpr struct my_origin : absolute_point_origin<my_origin, isq::time> {} my_origin;
quantity_point qp1 = sys_seconds{1s};
auto tp1 = to_chrono_time_point(qp1); // OK
quantity_point qp2 = chrono_point_origin<system_clock> + 1 * s;
auto tp2 = to_chrono_time_point(qp2); // OK
quantity_point qp3 = ts_origin + 1 * s;
auto tp3 = to_chrono_time_point(qp3); // OK
quantity_point qp4 = my_origin + 1 * s;
auto tp4 = to_chrono_time_point(qp4); // Compile-time Error (1)
```
1. `my_origin` is not defined in terms of `chrono_point_origin<Clock>`.
Here is an example of how interoperability described in this chapter can be used in practice:
```cpp
using namespace std::chrono;
std::chrono::sys_seconds ts_now = floor<seconds>(system_clock::now());
quantity_point start_time = ts_now;
quantity speed = 925. * km / h;
quantity distance = 8111. * km;
quantity flight_time = value_cast<int>((distance / speed).in(s));
quantity_point exp_end_time = start_time + flight_time;
std::chrono::sys_seconds ts_end = exp_end_time;
auto curr_time = zoned_time(current_zone(), ts_now);
auto mst_time = zoned_time("America/Denver", ts_end);
std::cout << "Takeoff: " << curr_time << "\n";
std::cout << "Landing: " << mst_time << "\n";
```
<!-- TODO fix the above to benefit from `value_cast` for `quantity_point` when implemented -->
The above may print the following output:
```cpp
Takeoff: 2023-11-18 13:20:54 UTC
Landing: 2023-11-18 15:07:01 MST
```

View File

@@ -145,7 +145,7 @@ nav:
- Pure Dimensional Analysis: users_guide/use_cases/pure_dimensional_analysis.md
- Working with Legacy Interfaces: users_guide/use_cases/working_with_legacy_interfaces.md
- Using Custom Representation Types: users_guide/use_cases/using_custom_representation_types.md
- Interoperability with Other Units Libraries: users_guide/use_cases/interoperability_with_other_units_libraries.md
- Interoperability with Other Libraries: users_guide/use_cases/interoperability_with_other_libraries.md
- Extending the Library: users_guide/use_cases/extending_the_library.md
- Examples:
- Tags Index: users_guide/examples/tags_index.md