forked from mpusz/mp-units
554 lines
21 KiB
Markdown
554 lines
21 KiB
Markdown
---
|
||
date: 2024-09-27
|
||
authors:
|
||
- mpusz
|
||
categories:
|
||
- Releases
|
||
comments: true
|
||
---
|
||
|
||
# mp-units 2.3.0 released!
|
||
|
||
**A new product version can be obtained from
|
||
[GitHub](https://github.com/mpusz/mp-units/releases/tag/v2.3.0) and
|
||
[Conan](https://conan.io/center/recipes/mp-units?version=2.3.0).**
|
||
|
||
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](../../release_notes.md#2.3.0).
|
||
|
||
<!-- more -->
|
||
|
||
## 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 (:boom: **breaking change** :boom:).
|
||
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```bash
|
||
conan install . -s compiler.cppstd=23 -b missing
|
||
```
|
||
|
||
or set it explicitly to `True` or `False` to force a specific configuration:
|
||
|
||
```bash
|
||
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`:
|
||
|
||
*[LEWGI]: Library Evolution Working Group Incubator
|
||
|
||
```cpp
|
||
// 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](https://lists.isocpp.org/std-proposals/2024/06/10118.php). As it was
|
||
reported, the code below may look correct, but it provides an invalid result:
|
||
|
||
```cpp
|
||
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`:
|
||
|
||
```cpp
|
||
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 (:boom: **breaking change** :boom:),
|
||
- 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
|
||
(:boom: **breaking change** :boom:).
|
||
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:
|
||
|
||
=== "Now"
|
||
|
||
```cpp
|
||
quantity q = delta<K>(300);
|
||
quantity_point qp = absolute<K>(300);
|
||
static_assert(q.in(deg_C) != qp.in(deg_C).quantity_from_zero());
|
||
```
|
||
|
||
=== "Before"
|
||
|
||
```cpp
|
||
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:
|
||
|
||
```cpp
|
||
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:
|
||
|
||
```cpp
|
||
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** (:boom: **breaking change** :boom:).
|
||
|
||
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.
|
||
|
||
=== "Now"
|
||
|
||
```cpp
|
||
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);
|
||
}
|
||
};
|
||
```
|
||
|
||
=== "Before"
|
||
|
||
```cpp
|
||
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:
|
||
|
||
=== "Now"
|
||
|
||
```cpp
|
||
inline constexpr struct degree final : named_unit<{u8"°", "deg"}, mag<pi> / mag<180> * si::radian> {} degree;
|
||
```
|
||
|
||
=== "Before"
|
||
|
||
```cpp
|
||
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:
|
||
|
||
```cpp
|
||
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:
|
||
|
||
=== "Now"
|
||
|
||
```cpp
|
||
quantity q = 1 * km + 1 * mi; // quantity<common_unit<international::mile, si::kilo_<si::metre>>{}, int>
|
||
```
|
||
|
||
=== "Before"
|
||
|
||
```cpp
|
||
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:
|
||
|
||
```cpp
|
||
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:
|
||
|
||
=== "Now"
|
||
|
||
```text
|
||
40771 ([1/25146] mi = [1/15625] km)
|
||
108167 ([1/50292] mi = [1/57875] nmi)
|
||
23 ([1/5] km/h = [1/18] m/s)
|
||
```
|
||
|
||
=== "Before"
|
||
|
||
```text
|
||
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()` (:boom: **breaking change** :boom:).
|
||
|
||
|
||
## 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.
|
||
|
||
=== "Now"
|
||
|
||
```cpp
|
||
quantity<one> inc(quantity<one> q) { return q + 1; }
|
||
void legacy(double) { /* ... */ }
|
||
|
||
if (auto q = inc(42); q != 0)
|
||
legacy(static_cast<int>(q));
|
||
```
|
||
|
||
=== "Before"
|
||
|
||
```cpp
|
||
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:
|
||
|
||
=== "Now"
|
||
|
||
```cpp
|
||
static_assert(10 * km / (5 * km) == 2);
|
||
const quantity gain = 1. / index;
|
||
```
|
||
|
||
=== "Before"
|
||
|
||
```cpp
|
||
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](https://github.com/fmtlib/fmt)) and remove functions contract
|
||
checking.
|
||
|
||
With the above assumptions, we can refactor our smoot example to:
|
||
|
||
```cpp hl_lines="2"
|
||
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](https://www.nist.gov/pml/owm/writing-si-metric-system-units)
|
||
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](https://github.com/search?q=repo%3Atorvalds%2Flinux+millidegree&type=code).
|
||
|
||
To prevent such issues, we decided to simplify the library's design and remove the
|
||
`unit_can_be_prefixed` type trait (:boom: **breaking change** :boom:).
|
||
|
||
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` (:boom: **breaking change** :boom:). 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.
|
||
|
||
|
||
## Error messages-related improvements
|
||
|
||
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 :wink:.
|
||
|
||
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:
|
||
|
||
=== "Now"
|
||
|
||
```text hl_lines="19"
|
||
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
|
||
```
|
||
|
||
=== "Before"
|
||
|
||
```text hl_lines="16"
|
||
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.
|