Interoperability with Other Libraries¶
mp-units makes it easy to cooperate with similar entities of other libraries. Whether
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_traitsfor externalquantity-like type, - a
quantity_point_like_traitsfor 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. Conversion traits should provide
a static data member convertible to bool. If the value is true, then the conversion is
explicit. Otherwise, if the value is false, implicit conversions will be allowed.
The names of the flags are as follows:
explicit_importto describe conversion from the external entity to the one in this library (import case),explicit_exportto describe conversion from the entity in this library to the external one (export case).
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:
referencestatic data member that provides the quantity reference (e.g., unit),reptype that specifies the underlying storage type,explicit_importstatic data member convertible toboolthat specifies that the conversion fromTto aquantitytype should happen explicitly (iftrue),explicit_exportstatic data member convertible toboolthat specifies that the conversion from aquantitytype toTshould happen explicitly (iftrue),to_numerical_value(T)static member function returning a quantity's raw value ofreptype,from_numerical_value(rep)static member function returningT.
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;
static constexpr bool explicit_import = false;
static constexpr bool explicit_export = true;
using rep = decltype(Meter::value);
static constexpr rep to_numerical_value(Meter m) { return m.value; }
static constexpr 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
doubletointis 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:
referencestatic data member that provides the quantity point reference (e.g., unit),point_originstatic data member that specifies the absolute point, which is the beginning of our measurement scale for our points,reptype that specifies the underlying storage type,explicit_importstatic data member convertible toboolthat specifies that the conversion fromTto aquantitytype should happen explicitly (iftrue),explicit_exportstatic data member convertible toboolthat specifies that the conversion from aquantitytype toTshould happen explicitly (iftrue),to_numerical_value(T)static member function returning a raw value of thequantitybeing the offset of the point from the origin,from_numerical_value(rep)static member function returningT.
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);
static constexpr bool explicit_import = false;
static constexpr bool explicit_export = true;
using rep = decltype(Timestamp::seconds);
static constexpr rep to_numerical_value(Timestamp ts) { return ts.seconds; }
static constexpr 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_traitsandquantity_point_like_traitsthat provide support for implicit conversions betweenstdandmp_unitstypes in both directions, chrono_point_origin<Clock>point origin forstdclocks,to_chrono_durationandto_chrono_time_pointdedicated 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_originis not defined in terms ofchrono_point_origin<Clock>.zeroth_point_originis 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: