Frequently Asked Questions¶
Why do we spell metre
instead of meter
?¶
This is how the BIPM defines it in the SI Brochure (British English spelling is used by default).
Why don't we use UDLs to create quantities?¶
Many reasons make UDLs a poor choice for a physical units library:
-
UDLs work only with literals (compile-time known values). Our observation is that, aside from unit tests, there are only a few compile-time known quantity values used in production code. Please note that for physical constants, we recommend using [Faster-than-lightspeed Constants] (../users_guide/framework_basics/faster_than_lightspeed_constants.md).
-
Typical implementations of UDLs tend to always use the widest representation type available. In the case of
std::chrono::duration
, the following is true:using namespace std::chrono_literals; auto d1 = 42s; auto d2 = 42.s; static_assert(std::is_same_v<decltype(d1)::rep, std::int64_t>); static_assert(std::is_same_v<decltype(d2)::rep, long double>);
When such a UDL is intermixed in arithmetic with any quantity type of a shorter representation type, it will always expand to the longest one. In other words, the long type propagates until all types use it everywhere.
-
While increasing the coverage for the library, we learned that many unit symbols conflict with built-in types or numeric extensions. A few of these are:
F
(farad),J
(joule),W
(watt),K
(kelvin),d
(day),l
orL
(litre),erg
,ergps
. Usage of the_
prefix would make it work for mp-units, but if the library is standardized, those naming collisions would be a significant issue. This is why we came up with the_q_
prefix that would becomeq_
after standardization (e.g.,42q_s
), which is not as nice anymore. -
UDLs with the same identifiers defined in different namespaces can't be disambiguated in the C++ language. If both SI and CGS systems define
_q_s
UDL for a second unit, then it would not be possible to specify which one to use if both namespaces are "imported" with using directives. -
Another drawback of UDLs is that they do not compose. A coherent unit of angular momentum would have a UDL specified as
_q_kg_m2_per_s
. Now imagine that we want to make every possible user happy. How many variations of that unit would we need to predefine for differently scaled versions of all unit ingredients? -
UDLs are also expensive to define and maintain. Typically, for each unit, we need two definitions: one for integral and another for floating-point representation. Before the V2 framework, the coherent unit of angular momentum was defined as:
constexpr auto operator"" _q_kg_m2_per_s(unsigned long long l) { gsl_Expects(std::in_range<std::int64_t>(l)); return angular_momentum<kilogram_metre_sq_per_second, std::int64_t>(static_cast<std::int64_t>(l)); } constexpr auto operator"" _q_kg_m2_per_s(long double l) { return angular_momentum<kilogram_metre_sq_per_second, long double>(l); }
Why can't I create a quantity by passing a number to a constructor?¶
In the mp-units library, a quantity class template has no publicly available constructor
taking a raw value. Such support is provided by std::chrono::duration
and was pointed out
to us as a red flag safety issue by several parties already.
Consider the following structure and a code using it:
Everything works fine for years until, at some point, someone changes the structure to:
The code continues to compile just fine, but all the calculations are now incorrect. This is why we decided not to follow this path.
In the mp-units library, both a number and a unit must always be explicitly provided in order to form a quantity.
Note
The same applies to the construction of quantity_point
using an explicit point origin.
To prevent similar safety issues during maintenance, initialization always requires
providing both a quantity
and a
PointOrigin
to use as a reference point.
Why a dimensionless quantity is not just a fundamental arithmetic type?¶
In the initial design of this library, the resulting type of division of two quantities was their common representation type:
First of all, this was consistent with
std::chrono::duration
behavior. Another reason was to avoid giving a false impression of a strong quantity
type
for something that looks and feels like a regular number. Also, all of the mathematical and
trigonometric functions worked fine out of the box with such representation types, so we did
not have to rewrite sin()
, cos()
, exp()
, and others.
However, feedback from production usage showed that such an approach is really bad for generic
programming. It is hard to handle the result of dividing (or multiplying) two quantities, as
it might be either a quantity or a fundamental type. If we want to raise such a result to
some power, we must use units::pow
or std::pow
depending on the resulting type. These
are just a few issues related to such an approach.
Moreover, suppose we divide quantities of the same dimension but with units of significantly different magnitudes. In that case, we may end up with a very small or a huge floating-point value, which may result in significant loss of precision. Returning a dimensionless quantity from such cases allows us to benefit from all the properties of scaled units and is consistent with the rest of the library.
Note
More information on the current design can be found in the Dimensionless Quantities chapter.
Why derived units order is not preserved from the multiplication?¶
It might be surprising, but the order of multiplication of quantities and units does not impact the order of components in the derived unit. Let's try the following example:
The above prints:
Some users might expect to see 42 kWh
or 42 kW h
in the output. That is not the case, and
for a very good reason. As stated in
[Simplifying the resulting symbolic expressions]
(../users_guide/framework_basics/interface_introduction.md#simplifying-the-resulting-symbolic-expressions),
to be able to reason about and simplify units, the library needs to order them in a consistent
way.
Maybe this default order could be improved, but according to international standards, there is no generic ordering rule. Various quantities use different, often domain-specific, ordering of derived unit components.
Let's see what SI says here:
Derived quantity | Symbol | Derived unit expressed in terms of base units |
---|---|---|
electric field strength | V m⁻¹ | kg m s⁻³ A⁻¹ |
electric charge density | C m⁻³ | A s m⁻³ |
exposure (x- and γ-rays) | C kg⁻¹ | A s kg⁻¹ |
However, there is a workaround. A user can define their own named unit for a derived unit and provide custom symbol text that suits the project's requirements. For example, the above case could be addressed with:
inline constexpr struct kilowatt_hour final : named_unit<"kWh", kW * h> {} kilowatt_hour;
inline constexpr auto kWh = kilowatt_hour;
With the above, we can refactor the above code to:
Both lines will produce an expected "42 kWh" unit in the output.
Important
Please note that this makes the entire "kWh" a single, indivisible entity that is not
subject to simplification rules. This means that 42 * kWh / (2 * h)
will result in
21 kWh/h
rather than 21 kW
. To get the latter, the user needs to explicitly provide a
new derived unit:
Why do the identifiers for concepts in the library use CamelCase
?¶
Initially, C++20 was meant to use CamelCase
for all the concept identifiers. All the concepts
from the std::ranges
library were merged with such names into the standard document draft.
Frustratingly, CamelCase
concepts got dropped from the C++ standard at the last moment before
releasing C++20. Now, we are facing the predictable consequences of running out of names.
While some concepts in the library could be easily named with a standard_case
, there are
some that are hard to distinguish from the corresponding type names, such as Quantity
,
QuantityPoint
, QuantitySpec
, or Reference
. This is why we decided to use CamelCase
consistently for all the concept identifiers to make it clear when we are talking about a type
or a concept identifier.
However, we are aware that this might be a temporary solution. If the library gets
standardized, we can expect the ISO C++ Committee to bikeshed/rename all of the concept
identifiers to a standard_case
, even if it results in harder-to-understand code.
Note
In case you have a good idea of how to rename
existing concepts to the standard_case
,
please let us know in the associated GitHub Issue.
Why UTF-8 quantity symbols are used by default instead of portable characters?¶
Both C++ and ISO 80000 are standardized by the ISO.
ISO 80000 and the SI
standards specify UTF-8 symbols as the official unit names for some quantities (e.g. Ω
symbol
for the resistance quantity). As the mp-units library will be proposed for standardization
as part of the C++ Standard Library, we have to follow the rules and be consistent with ISO
specifications.
Note
We do understand engineering reality and the constraints of some environments. This is why the library has the option of Portable Quantity Symbols.
Why don't we have CMake options to disable the building of tests and examples?¶
Over time, many people provided PRs proposing adding options to build tests and examples conditionally. Here are a few examples:
- Add CMake options for disabling docs, examples and tests
- build: add options to disable part of the build
- CMake Refactoring and Option Cleanup
We admit this is a common practice in the industry, but we also believe this is a bad pattern.
First, the only need for such options comes when a user wants to use add_subdirectory()
in
CMake to handle dependencies. Such an approach does not scale and should be discouraged. There
is little need for such a practice in times when we have dedicated package managers like Conan.
Secondly, we have observed that many people focus on disabling "unneeded" subdirectories from compilation, but they do not see or address the biggest issue, which is polluting the user's build environment with our development-specific settings. Propagating our restrictive compilation flags to the user's project is not the best idea, as it might cause a lot of harm if their project stops compiling because of that.
Last but not least, not having those options is intentional. The top-level CMakeLists.txt file should only be used by mp-units developers and contributors as an entry point for the project's development. We want to ensure that everyone builds ALL the code correctly before pushing a commit. Having such options would allow unintended issues to leak into PRs and CI.
This is why our projects have two entry points:
- ./CMakeLists.txt is to be used by projects developers to build ALL the project code with really restrictive compilation flags,
- ./src/CMakeLists.txt contains only a pure library definition and should be used by the
customers that prefer to use CMake's
add_subdirectory()
to handle the dependencies.
Note
For more details on this please refer to the CMake + Conan: 3 Years Later - Mateusz Pusz lecture that Mateusz Pusz gave at the C++Now 2021 conference.