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 externalquantity
-like type, - a
quantity_point_like_traits
for externalquantity_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:
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 ofrep
type packed in eitherconvert_explicitly
orconvert_implicitly
wrapper.from_numerical_value(rep)
static member function returningT
packed in eitherconvert_explicitly
orconvert_implicitly
wrapper.
For example, for our Meter
type, we could provide the following:
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
concept is satisfied:
and we can write the following:
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:
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));
- Truncation of value while converting from meters to kilometers.
- Conversion of
double
toint
is not value-preserving. - Truncation of value while converting from millimeters to meters.
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.
As we described in The Affine Space 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_numerical_value(T)
static member function returning a raw value of thequantity
being the offset of the point from the origin packed in eitherconvert_explicitly
orconvert_implicitly
wrapper.from_numerical_value(rep)
static member function returningT
packed in eitherconvert_explicitly
orconvert_implicitly
wrapper.
For example, for our Timestamp
type, we could provide the following:
template<>
struct mp_units::quantity_point_like_traits<Timestamp> {
static constexpr auto reference = si::second;
static constexpr auto point_origin = default_point_origin(reference);
using rep = decltype(Timestamp::seconds);
static constexpr convert_implicitly<rep> to_numerical_value(Timestamp ts) { return ts.seconds; }
static constexpr convert_explicitly<Timestamp> from_numerical_value(rep v) { return Timestamp(v); }
};
After that, we can check that the QuantityPointLike
concept is satisfied:
and we can write the following:
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_zero() << "\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
- specifies quantities of time,std::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/systems/si/chrono.h file to benefit from it. This file provides:
- partial specializations of
quantity_like_traits
andquantity_point_like_traits
that provide support for implicit conversions betweenstd
andmp_units
types in both directions, chrono_point_origin<Clock>
point origin forstd
clocks,to_chrono_duration
andto_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:
inline constexpr struct ts_origin final : relative_point_origin<chrono_point_origin<system_clock> + 1 * h> {} ts_origin;
inline constexpr struct my_origin final : absolute_point_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)
quantity_point qp5{1 * s};
auto tp5 = to_chrono_time_point(qp5); // Compile-time Error (2)
my_origin
is not defined in terms ofchrono_point_origin<Clock>
.zeroth_point_origin
is not defined in terms ofchrono_point_origin<Clock>
.
Here is an example of how interoperability described in this chapter can be used in practice:
using namespace 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 = distance / speed;
quantity_point exp_end_time = start_time + flight_time;
sys_seconds ts_end = value_cast<int>(exp_end_time.in(s));
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";
The above may print the following output: