Skip to content

mp-units 2.3.0 released!

A new product version can be obtained from GitHub and Conan.

This release fine-tunes many key features of the library. This post describes the most interesting improvements, while a much longer list of the changes introduced by the new version can be found in our Release Notes.

CMake and Conan options changed

During the review on the ConanCenter, we got feedback that we should improve the handling of options for which value is automatically determined based on the current configuration. Instead of explicitly setting the auto value, we defer the choice between True/False until the configuration stage and set it there once all the settings are known. auto value for such option was removed (💥 breaking change 💥).

If you didn't set any value at the command line for such options, everything stays the same for you. However, some changes are needed if you explicitly used auto like below:

conan install . -o 'mp-units:std_format=auto' -s compiler.cppstd=23 -b missing

Now you have to either skip such an option to keep automatic deduction:

conan install . -s compiler.cppstd=23 -b missing

or set it explicitly to True or False to force a specific configuration:

conan install . -o 'mp-units:std_format=True' -s compiler.cppstd=23 -b missing

MSVC compiler support

The MSVC compiler has the most bugs in the C++20 support from all our compilers. However, with this release, we could apply many workarounds to the library's code to make it work. 🎉

Please note those workarounds were only applied to the library's code and not to our unit tests and examples. Trying to build the entire project on MSVC will inevitably fail to compile unless the MSVC bugs are resolved. MSVC developers still have some work to do.

Representation type template parameter added to value conversion functions

Previously, changing a representation type was only possible with a value_cast<NewRep>(q) non-member function while a change of unit was supported by all value_cast<NewU>(q), q.in(NewU), and q.force_in(NewU). The rationale for it was that passing an explicit type to a member function template requires a template disambiguator when we are dealing with a dependent name (e.g., quantity type is determined based on a template parameter).

During a discussion in LEWGI at the St. Louis WG21 Meeting, we decided to provide such additional overloads despite possible issues when a dependent name is used. In such case, a user needs to provide a template disambiguator or switch back to using value_cast:

// non-dependent name
auto f(quantity<m, int> q)             { return q.in<double>(km); }
auto g(quantity<m, int> q)             { return value_cast<double, km>(q); }

// dependent name
auto h(QuantityOf<isq::length> auto q) { return q.template in<double>(km); }
auto i(QuantityOf<isq::length> auto q) { return value_cast<double, km>(q); }

The table below provides all the value conversion functions in mp-units that may be run on x being the instance of either quantity or quantity_point:

Forcing Representation Unit Member function Non-member function
No Same u x.in(u)
No T Same x.in<T>()
No T u x.in<T>(u)
Yes Same u x.force_in(u) value_cast<u>(x)
Yes T Same x.force_in<T>() value_cast<T>(x)
Yes T u x.force_in<T>(u) value_cast<u, T>(x) or value_cast<T, u>(x)

Quantity reference specifiers

The features described in this chapter directly solve an issue raised on std-proposals reflector. As it was reported, the code below may look correct, but it provides an invalid result:

quantity Volume = 1.0 * m3;
quantity Temperature = 28.0 * deg_C;
quantity n_ = 0.04401 * kg / mol;
quantity R_boltzman = 8.314 * N * m / (K * mol);
quantity mass = 40.0 * kg;
quantity Pressure = R_boltzman * Temperature.in(K) * mass / n_ / Volume;
std::cout << Pressure << "\n";

The problem is related to the accidental usage of a quantity rather than quantity_point for Temperature. This means that after conversion to kelvins, we will get 28 K instead of the expected 301.15 K, corrupting all further calculations.

A correct code should use a quantity_point:

quantity_point Temperature(28.0 * deg_C);

This might be an obvious thing for domain experts, but new users of the library may not be aware of the affine space abstractions and how they influence temperature handling.

After a lengthy discussion on handling such scenarios, we decided to:

  • make the above code deprecated (💥 breaking change 💥),
  • provide an alternative way to create a quantity with the delta quantity construction helper.

Here are the main points of this new design:

  1. All references/units that specify point origin in their definition (i.e., si::kelvin,    si::degree_Celsius, and usc::degree_Fahrenheit) are excluded from the multiply syntax (💥 breaking change 💥).
  2. A new delta quantity construction helper is introduced:

    • delta<m>(42) results with a quantity<si::metre, int>,
    • delta<deg_C>(5) results with a quantity<si::deg_C, int>.
  3. A new absolute quantity point construction helper is introduced:

    • absolute<m>(42) results with a quantity_point<si::metre, zeroth_point_origin<kind_of<isq::length>>{}, int>,
    • absolute<deg_C>(5) results with a quantity<si::metre, si::ice_point, int>.

Info

Please note that si::kelvin is also excluded from the multiply syntax to prevent the following surprising issues:

quantity q = delta<K>(300);
quantity_point qp = absolute<K>(300);
static_assert(q.in(deg_C) != qp.in(deg_C).quantity_from_zero());
quantity q(300 * K);
quantity_point qp(300 * K);
static_assert(q.in(deg_C) != qp.in(deg_C).quantity_from_zero());

We believe that the code enforced with new utilities makes it much easier to understand what happens here.

With such changes to the interface design, the offending code will not compile as initially written. Users will be forced to think more about what they write. To enable the compilation, the users have to create explicitly:

  • a quantity_point (the intended abstraction in this example) with any of the below syntaxes:

    quantity_point Temperature = absolute<deg_C>(28.0);
    auto Temperature = absolute<deg_C>(28.0);
    quantity_point Temperature(delta<deg_C>(28.0));
    
  • a quantity (an incorrect abstraction in this example) with:

    quantity Temperature = delta<deg_C>(28.0);
    auto Temperature = delta<deg_C>(28.0);
    

Thanks to the new design, we can immediately see what happens here and why the result might be incorrect in the second case.

quantity_point_like_traits are based on numerical value instead of a quantity

In this release, we decided to fine-tune the traits that customize the conversion between custom quantity point types and the ones provided with mp-units (💥 breaking change 💥).

Previously, such type traits were based on the quantity type. This was inconsistent with quantity_like_traits, that is working on raw values. Also, there are cases where a custom quantity point abstraction is not modelled with a quantity type. In such cases, the previous approach required additional types to be introduced for no good reason.

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);
  }
};
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<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));
  }
};

Note

The old behavior is deprecated and will be removed in future releases.

mag<pi>

With this release, we introduced a new strongly-typed constant to create a magnitude involving scaling by pi. The solution used before was not consistent with magnitudes of integral values and also was leaking a floating-point value of std::numbers::pi_v<long double> to the resulting magnitude type. With the new approach, this is no longer the case, and the user-facing interface is more consistent:

inline constexpr struct degree final : named_unit<{u8"°", "deg"}, mag<pi> / mag<180> * si::radian> {} degree;
inline constexpr struct degree final : named_unit<{u8"°", "deg"}, mag_pi / mag<180> * si::radian> {} degree;

Note

The old mag_pi helper is marked as deprecated and will be removed in future releases.

Common units

Adding or subtracting two quantities of different units will force the library to find a common unit for those. This is to prevent data truncation. For the cases when one of the units is an integral multiple of the another, the resulting quantity will use a "smaller" one in its result. For example:

static_assert((1 * kg + 1 * g).unit == g);
static_assert((1 * km + 1 * mm).unit == mm);
static_assert((1 * yd + 1 * mi).unit == yd);

However, in many cases an arithmetic on quantities of different units will result in a yet another unit. This happens when none of the source units is an integral multiple of another. In such cases, the library returns a special type that denotes that we are dealing with a common unit of such an equation.

Previously we returned a scaled unit calculated against our arbitrarily appointed reference unit. This resulted often in a long and messy type exposing the prime-factorized magnitude of the unit (implementation detail). In this release, we introduced a new common_unit wrapper for such cases:

quantity q = 1 * km + 1 * mi;  // quantity<common_unit<international::mile, si::kilo_<si::metre>>{}, int>
quantity q = 1 * km + 1 * mi;  // quantity<scaled_unit<magnitude<power_v<2, 3>{}, power_v<5, -3>{}>{}, si::metre>{}, int>

Note

A user should never explicitly instantiate a common_unit class template. The library's framework will do it based on the provided quantity equation.

Such units need special printing rules for their symbols. As they represent a minimum set of common units resulting from the addition or subtraction of multiple quantities, from this release, we print all of them as a scaled version of the source unit. Previously we were printing them relative to some arbitrary reference unit (implementation detail) that often was not spelled by the user at all in the source code. For example the following:

std::cout << 1 * km + 1 * mi << "\n";
std::cout << 1 * nmi + 1 * mi << "\n";
std::cout << 1 * km / h + 1 * m / s << "\n";

will print:

40771 ([1/25146] mi = [1/15625] km)
108167 ([1/50292] mi = [1/57875] nmi)
23 ([1/5] km/h = [1/18] m/s)
40771 [8/125] m
108167 [4/125] m
23 [1/18] m/s

Thanks to the above, it might be easier for the user to reason about the magnitude of the resulting unit and its impact on the value stored in the quantity.

Info

In order to provide common_unit strong type unit wrapper we had to rename all the common_XXX() functions to get_common_XXX() (💥 breaking change 💥).

Superpowers of the unit one

In this release, we also added a long-awaited change. From now on a quantity of a unit one can be:

  • implicitly constructed from the raw value,
  • explicitly converted to a raw value,
  • compared to a raw value.
quantity<one> inc(quantity<one> q) { return q + 1; }
void legacy(double) { /* ... */ }

if (auto q = inc(42); q != 0)
  legacy(static_cast<int>(q));
quantity<one> inc(quantity<one> q) { return q + 1 * one; }
void legacy(double) { /* ... */ }

if (auto q = inc(42 * one); q != 0 * one)
  legacy(q.numerical_value_in(one));

This property also expands to usual arithmetic operators.

With the above change, we can now achieve the same results in a terser way:

static_assert(10 * km / (5 * km) == 2);
const quantity gain = 1. / index;
static_assert(10 * km / (5 * km) == 2 * one);
const quantity gain = 1. / index * one;

Note

Those rules do not apply to all the dimensionless quantities. It would be unsafe and misleading to allow such operations on units with a magnitude different than 1 (e.g., percent or radian).

import std; support

This release brings experimental support for import std;. The only compiler that supports it for now is clang-18+. Until all the compilers start to support it and CMake removes the experimental tag from this feature, we will also keep it experimental.

As all of the C++ compilers are buggy for now, it is not allowed to bring the same definitions through the import std; and regular header files. This applies not only to the current project but also to all its dependencies. This is why, in order to use it with mp-units, we need to disable all the dependencies as well (enforced with conanfile.py). It means that we have to use std::format (instead of fmtlib) and remove functions contract checking.

With the above assumptions, we can refactor our smoot example to:

import mp_units;
import std;

using namespace mp_units;

inline constexpr struct smoot final : named_unit<"smoot", mag<67> * usc::inch> {} smoot;

int main()
{
  constexpr quantity dist = 364.4 * smoot;
  std::println("Harvard Bridge length = {::N[.1f]} ({::N[.1f]}, {::N[.2f]}) ± 1 εar",
               dist, dist.in(usc::foot), dist.in(si::metre));
}

unit_can_be_prefixed removed

Previously, the unit_can_be_prefixed type trait was used to limit the possibility to prefix some units that are officially known as non-prefixable (e.g., hour, minute).

It turned out that it is not easy to determine whether some units can be prefixed. For example, for degree Celsius, the ISO 80000-5 standard explicitly states:

Prefixes are not allowed in combination with the unit °C.

On the other hand this NIST page says:

Prefix symbols may be used with the unit symbol ºC and prefix names may be used with the unit name “degree Celsius.” For example, 12 mºC (12 millidegrees Celsius) is acceptable.

It seems that it is also a common engineering practice.

To prevent such issues, we decided to simplify the library's design and remove the unit_can_be_prefixed type trait (💥 breaking change 💥).

From now on, every named unit in the library can be prefixed with the SI or IEC prefix.

iec80000 system renamed to iec

As we mentioned IEC already, in this release, we decided to rename the name of the system and its corresponding namespace from iec80000 to iec (💥 breaking change 💥). This involves renaming of a defining header file and of the namespace it provides.

With this change it should be easier to type the namespace name. This name is also more correct for some quantities and units that are introduced by IEC but not necessarily in the ISO/IEC 80000 series of documents (e.g., iec::var).

Note

The old iec80000 namespace in iec8000.h is marked as deprecated and will be removed in future releases.

The readability of compile-time error messages is always a challenge for generic C++ libraries. However, for quantities and units library, generating readable errors is the most important requirement. If you do not make errors, you do not need such a library in your project 😉.

This is why we put lots of effort into improving here. Besides submitting compiler bugs to improve on their part, we also try to do our best here.

Some compilers do not present the type resulting from calling a function within a template argument. This ends up with statements like get_quantity_spec(si::second{}) in the error message. Some less experienced users of the library may not know what this mean, and then why the conversion error happens.

To improve this, we injected additional helper concepts into the definitions. It results with a bit longer but a more readable error in the end.

For example:

error: no matching member function for call to 'in'
   15 | const quantity time_to_goal = (distance * speed).in(s);
      |                               ~~~~~~~~~~~~~~~~~~~^~
note: candidate template ignored: constraints not satisfied [with ToU = struct second]
  221 |   [[nodiscard]] constexpr QuantityOf<quantity_spec> auto in(ToU) const
      |                                                          ^
note: because 'detail::UnitCompatibleWith<si::second, unit, quantity_spec>' evaluated to false
  219 |   template<detail::UnitCompatibleWith<unit, quantity_spec> ToU>
      |            ^
note: because '!AssociatedUnit<si::second>' evaluated to false
  164 |   (!AssociatedUnit<U> || UnitOf<U, QS>) && detail::UnitConvertibleTo<FromU, U{}>;
      |    ^
note: and 'UnitOf<si::second, kind_of_<derived_quantity_spec<power<isq::length, 2>, per<isq::time> > >{}>' evaluated to false
  164 |   (!AssociatedUnit<U> || UnitOf<U, QS>) && detail::UnitConvertibleTo<FromU, U{}>;
      |                          ^
note: because 'detail::QuantitySpecConvertibleTo<get_quantity_spec(si::second{}), kind_of_<derived_quantity_spec<power<isq::length, 2>, per<isq::time> > >{}>' evaluated to false
  141 |                  detail::QuantitySpecConvertibleTo<get_quantity_spec(U{}), QS> &&
      |                  ^
note: because 'implicitly_convertible(kind_of_<struct time>{}, kind_of_<derived_quantity_spec<power<isq::length, 2>, per<isq::time> > >{})' evaluated to false
  151 |   implicitly_convertible(From, To);
      |   ^
1 error generated.
Compiler returned: 1
error: no matching member function for call to 'in'
   15 | const quantity time_to_goal = (distance * speed).in(s);
      |                               ~~~~~~~~~~~~~~~~~~~^~
note: candidate template ignored: constraints not satisfied [with U = struct second]
  185 |   [[nodiscard]] constexpr QuantityOf<quantity_spec> auto in(U) const
      |                                                          ^
note: because 'detail::UnitCompatibleWith<si::second, unit, quantity_spec>' evaluated to false
  183 |   template<detail::UnitCompatibleWith<unit, quantity_spec> U>
      |            ^
note: because '!AssociatedUnit<si::second>' evaluated to false
  207 |   (!AssociatedUnit<U> || UnitOf<U, QS>)&&(detail::have_same_canonical_reference_unit(U{}, U2));
      |    ^
note: and 'UnitOf<si::second, kind_of_<derived_quantity_spec<power<isq::length, 2>, per<isq::time> > >{}>' evaluated to false
  207 |   (!AssociatedUnit<U> || UnitOf<U, QS>)&&(detail::have_same_canonical_reference_unit(U{}, U2));
      |                          ^
note: because 'implicitly_convertible(get_quantity_spec(si::second{}), kind_of_<derived_quantity_spec<power<length, 2>, per<time> > >{})' evaluated to false
  187 |   implicitly_convertible(get_quantity_spec(U{}), QS) &&
      |   ^
1 error generated.
Compiler returned: 1

Note

The above error messages were stripped a bit of the additional information (file name, namespace name, nested curlies) to provide better readability.

Comments