| Document #: | P3045R8 [Latest] [Status] |
| Date: | 2026-05-12 |
| Project: | Programming Language C++ |
| Audience: |
LEWG Library Evolution Working Group SG6 Numerics SG16 Unicode SG20 Education |
| Reply-to: |
Mateusz Pusz (Train
IT) <mateusz.pusz@gmail.com> Dominik Berner <dominik.berner@gmail.com> Johel Ernesto Guerrero Peña <johelegp@gmail.com> Chip Hogg (Aurora Innovation) <charles.r.hogg@gmail.com> Nicolas Holthaus <nholthaus@gmail.com> Roth Michaels (Native Instruments) <isocxx@rothmichaels.us> Vincent Reverdy <vince.rev@gmail.com> |
named_unit<"smoot", mag<67> * usc::inch>quantity type364.4 * smootdist.in(si::metre){::N[.1f]}is_kind
fixed_stringsymbol_textspace_before_unit_symbol
customization pointRepresentationOf concept
updated.quantity_from_zero()
renamed to quantity_from_unit_zero(),
and zeroth_point_origin<QS>
renamed to natural_point_origin<QS>.final
keyword removed from all symbolic-expression type definitions throughout
code examples.is_kind connections in quantities_of_dimensionless.svg
now use dotted lines.is_kind chapter added.AssociatedUnit concept
removed.PrefixableUnit concept
added.0 replaced
the discussion of non-ideal alternativesnamed_constant support
added.pi
mag_constant renamed to
pi_c to allow
π be an identifier for a
named_constant.default_denominator renamed to
default_solidus in
unit_symbol_solidus enum.quantity_values trait renamed to
representation_values.quantity::one()
static member function removed after LEWGI and SG6 feedback.one chapter updated.std::chrono::duration
chapter addedtext_encoding renamed to
character_set.mp_units namespace usage
replaced with std.absolute creation helper renamed
to point.half_high_dot added.QuantitySpecOf and
UnitOf concepts simplified.QuantityOf and
QuantityPointOf concepts constrained
with ReferenceOf.(...)
instead of
[...] in the
text output.symbol_text.unit-symbol-solidus
alternative grammars added.std-format-spec
chapter added.𝜋 changed to
π after SG16 feedback.quantity_like_traits,
quantity_point_like_traits,
QuantityLike, and
QuantityPointLike refactored to use
explicit_import and
explicit_export flags instead of
wrapping tag types.RepresentationOf concept
refactored.ascii renamed to
portable and
unicode renamed to
utf8.space_before_unit_symbol
alternatives chapter added.force_in(U)
chapter added.quantity::rep
chapter added.std::chrono
abstractions chapter extended.default_point_origin<Reference>,
quantity_from_zero(),
and zeroth_point_origin<QuantitySpec>]
chapter added.quantity_spec chapters
added.reference chapter added.𝜋 added as an alias for
pifinal.delta and
absolute creation helpers added to
improve readability of the affine space entities creation.std::remove_const
was not needed in prefixes definitions.inline
dropped from inline constexpr
variable templates (based on CWG2387)q.in<Representation>(unit)
support added despite possible
template
disambiguator drawbacks.quantity_point_like_traits
member functions refactored to not depend on
quantity-like abstractions.unit_can_be_prefixed removed
from the design.delta and
absolute creation helpers] chapter
added.one chapter added.mag_pi replaced with mag<pi>value_cast overloads.qp.quantity_from_zero()
does not work for user’s named origins anymore.qp.quantity_from()
now works with other quantity points as well.basic_symbol_text renamed to
symbol_text.[[nodiscard]]
removed from symbol_text.symbol_text constructors taking
string literals made
consteval.symbol_text now always stores
char8_t and
char
versions of symbols.u8) literal.mag<ratio{N, D}>
replaced with mag_ratio<N, D>
so the ratio type becomes the
implementation detail rather than the public interface of the
librarySeveral groups in the ISO C++ Committee reviewed the “P1935: A C++ Approach to Physical Units” [P1935R2] proposal in Belfast 2019 and Prague 2020. All those groups expressed interest in the potential standardization of such a library and encouraged further work. The authors also got valuable initial feedback that highly influenced the design of the V2 version of the [mp-units] library.
In the following years, the library’s authors focused on getting more feedback from the production about the design and developed version 2 of the [mp-units] library that resolves the issues raised by the users and Committee members. The features and interfaces of this version are close to being the best we can get with the current version of the C++ language standard.
This paper is authored by the [mp-units] library developers, the authors of other actively maintained similar libraries on the market, and other active members of the C++ physical quantities and units community who have worked on this subject for many years. We join our forces to say with one voice that we deeply care about standardizing such features as a part of the C++ Standard Library. Based on our long and broad experience in the subject, we agree that the interfaces we will provide in the upcoming proposals are the best we can get today in the C++ language.
During the Kona 2023 ISO C++ Committee meeting, we got repeating feedback that there should be one big, unified paper with all the contents inside. Addressing this requirement, this paper adds a detailed design description and also includes the most important parts of [P2980R1], [P2981R1], and [P2982R1]. With this, we assume that [P2981R1] and [P2982R1] are superseded by this paper. The plan and scope described in [P2980R1] might still be updated based on the current progress and feedback from the upcoming discussions.
Note: This paper is incomplete and many chapters are still missing. It is published to gather early feedback and possibly get acceptance for the major design decisions of the library. More details will arrive in the next revisions of this paper.
This proposal is designed to serve multiple audiences, from beginners learning type-safe programming to framework developers extending the library’s core. The Teachability chapter includes audience tables (following [P1700R0]) that map library features to four distinct user populations: Application Developers (millions), Unit Authors (tens of thousands), Domain Modelers (thousands), and Deep Integrators (hundreds).
Most readers—whether evaluating the proposal for standardization or planning to use the library—need only understand the core usage patterns described in API overview and Usage examples. The vast majority of users will interact solely with predefined units from standard systems (SI, CGS, etc.) using intuitive multiply syntax and automatic unit conversions. More advanced topics like custom quantity hierarchies, affine space abstractions, and framework extension points are clearly marked and can be deferred or skipped entirely by casual users.
This paper describes and defines a generic framework for quantities and units library. Such framework should allow modeling various systems of quantities and units customized according to specific user’s needs. Such systems can be embraced with the affine space abstractions to provide dimension-, unit-, representation-, quantity kind-, quantity-, and affine space-safe abstractions for many industries.
Beyond physical units, this library may also provide long-awaited functionality for the C++ community. It enables creating strongly-typed wrappers for fundamental types to prevent bugs that arise from accidentally mixing semantically different values.
Even if mentioned, this paper does not propose standardizing any systems of quantities or units. Such definitions will arrive in subsequent proposals.
In the extreme case, we can even discuss just providing a library framework in the first C++ standard and standardize systems and additional utilities (e.g., math) in the next iterations.
This document consistently uses the official metrology vocabulary defined in the [ISO/IEC Guide 99] and [JCGM 200:2012].
This change is purely additive. It does not change, fix, or break any of the existing tools in the C++ standard library.
std::chrono
types and
std::ratioThe only interaction of this proposal with the C++ standard
facilities is the compatibility mode with
std::chrono
types (duration and
time_point) described in Interoperability
with the
std::chrono
abstractions.
We should also mention the potential confusion of users with having two different ways to deal with time abstractions in the C++ standard library. If this proposal gets accepted:
std::chrono
abstractions together with
std::ratio
should be used primarily to deal with clocks, calendars, and threading
facilities,The features in this chapter are heavily used in the library but are not domain-specific. Having them standardized (instead of left as exposition-only) could not only improve this library’s specification, but also serve as an essential building block for tools in other domains that we can get in the future from other authors.
Feature
|
Priority
|
Papers
|
Description
|
|---|---|---|---|
std::basic_fixed_string |
1 | [P3094R6] | String-like structural type with inline storage (can be used as an NTTP). |
| Extending NTTPs | 2 | [P3380R1] | Avoiding public members in classes. |
| User control of associated entities | 2 | [P2822R2] | ADL for NTTP parameters. |
template = delete |
2 | [P2041R1] | Deleting primary variable templates for customization points. |
| Preventing value truncation | 2 | [P0870R5], [P2509R1] | Type traits stating if the conversion from one type to another is narrowing/value preserving or not. |
| Format context rebind | 2 | ??? | Possibility to override the format string in the parse and format contexts. |
Grouping numbers in std-format-spec |
2 | ??? | Extensions to std-format-spec. |
| Number concepts | 2 | [P3003R0] | Concepts for vector- and point-space numbers. |
| SFINAEable constexpr exceptions | 3 | [P3679R0] | Much improved error messages. |
| A Simple Approach to Universal Template Parameters | 3 | [P2989R2] | Uniform handling of types and NTTPs in the generic interfaces. |
| Compile-time prime numbers | 3 | [P3133R0] | Compile-time facilities to break any integral value to a product of prime numbers and their powers. |
| Constrained Numbers | 3 | [P2993R0] | Numerical type wrappers with values bounded to a provided interval (optionally with wraparound semantics). |
Priorities used above:
Dominik has 15+ years of C++ experience, primarily in regulated Med-Tech projects where type safety is critical. He is the author of [SI library], a physical quantities library focused on type-safe conversions and zero-overhead computation. His extensive experience debugging issues caused by incorrect primitive type usage in safety-critical contexts motivated his work on standardizing quantities and units. He joined this proposal to contribute his practical experience with production safety requirements and strongly-typed interfaces.
Johel is a computing systems engineer and C++ programmer since 2014.
He was a core contributor to [nholthaus/units] v3 (2018-2020), where
he remodeled interfaces after std::chrono::duration
and improved error messages. Since 2020, he has been a key contributor
to [mp-units], designing and implementing
quantity_point (affine space) and
quantity_kind (distinct quantities
of same dimension). He ensures the library’s alignment with [JCGM
200:2012] and [ISO/IEC 80000] metrology standards,
bringing rigorous domain expertise to the proposal.
Chip Hogg is a Staff Software Engineer at Aurora Innovation working on autonomous vehicle Motion Planning. He holds a PhD in Physics from Carnegie Mellon and worked as a staff scientist at NIST. He is the creator and lead developer of [Au], a widely-adopted zero-dependency units library with novel features including vector space magnitudes and adaptive overflow protection. His pioneering work on unit-safe interfaces at Uber ATG (2018) and Aurora (2021) established best practices for safe API design with quantities. He contributes his extensive production experience and focus on developer ergonomics to this proposal.
Nicolas holds a B.S. in Computer Engineering (Summa Cum Laude) from Northwestern University. He designed real-time C++ software for aircraft survivability simulation at the U.S. Naval Air Warfare Center and has continued in safety-critical domains at MIT Lincoln Laboratory and STR. He is the author of [nholthaus/units], one of the most widely adopted C++ units libraries, with extensive deployment in modeling & simulation, agriculture, and geodesy. His practical experience with production units libraries across multiple domains informs the safety and usability requirements of this proposal.
Roth Michaels is a Principal Software Engineer at Native Instruments, working on audio and music software. With a degree in music composition and over a decade of experience in digital signal processing, analog audio, and acoustics, he brings unique domain expertise to the proposal. He has researched and prototyped domain-specific quantities and units for audio (samples, beats, frequency, power ratios) using [mp-units], contributing perspective on logarithmic quantities, non-linear relationships, and the needs of domains beyond traditional physics and engineering.
Mateusz is the creator and lead developer of [mp-units], the most popular and actively maintained C++ physical quantities and units library. With over 10 years of experience in the domain dating back to the [LK8000] flight computer project, he designed [mp-units] to leverage modern C++ (Concepts, NTTP) for safety and user experience. He has unified the C++ quantities community by bringing together authors of other major libraries to collaborate on this proposal. Through numerous conference talks and workshops, he has become a recognized expert in physical quantities and units for C++.
Vincent is an astrophysicist and computer scientist at the French National Centre for Scientific Research (CNRS) and a member of the French delegation to the ISO C++ Committee. He authored [P1930R0] providing foundational context for quantities and units in C++. His current research focuses on the mathematical formalization of systems of quantities and units as an interdisciplinary problem spanning physics, mathematics, and computer science. He brings rigorous theoretical foundations and scientific computing perspective to this proposal.
This chapter describes why we believe that physical quantities and units should be part of a C++ Standard Library.
It is no longer only the space industry or experienced pilots that benefit from the autonomous operations of some machines. We live in a world where more and more ordinary people trust machines with their lives daily. In the near future, we will be allowed to sleep while our car autonomously drives us home from a late party. As a result, many more C++ engineers are expected to write life-critical software today than it was a few years ago. However, writing safety-critical code requires extensive training and experience, both of which are in short demand. While there exists some standards and guidelines such as MISRA C++ [MISRA C++] with the aim of enforcing the creation of safe code in C++, they are cumbersome to use and tend to shift the burden on the discipline of the programmers to enforce these. At the time of writing, the C++ language does not change fast enough to enforce safe-by-construction code.
One of the ways C++ can significantly improve the safety of applications being written by thousands of developers is by introducing a type-safe, well-tested, standardized way to handle physical quantities and their units. The rationale is that people tend to have problems communicating or using proper units in code and daily life. Numerous expensive failures and accidents happened due to using an invalid unit or a quantity type.
The most famous and probably the most expensive example in the software engineering domain is the Mars Climate Orbiter that in 1999 failed to enter Mars’ orbit and crashed while entering its atmosphere [Mars Orbiter]. This is one of many examples here. People tend to confuse units quite often. We see similar errors occurring in various domains over the years:
20 °C above
the average temperature was converted to
68 °F. The
actual temperature increase was
32 °F, not
68 °F [The Guardian].The safety subject is so vast and essential by itself that we dedicated an entire [Safety features] chapter of this paper that discusses all the nuances in detail.
We standardized many library features mostly used in the implementation details (fmt, ranges, random-number generators, etc.). However, we believe that the most important role of the C++ Standard is to provide a standardized way of communication between different vendors.
Let’s imagine a world without
std::string
or
std::vector.
Every vendor has their version of it, and of course, they are highly
incompatible with each other. As a result, when someone needs to
integrate software from different vendors, it turns out to be an
unnecessarily arduous task.
Introducing std::chrono::duration
and std::chrono::time_point
improved the interfaces a lot, but time is only one of many quantities
that we deal with in our software on a daily basis. We desperately need
to be able to express more quantities and units in a standardized way so
different libraries get means to communicate with each other.
If Lockheed Martin and NASA could have used standardized vocabulary types in their interfaces, maybe they would not interpret pound-force seconds as newton seconds, and the [Mars Orbiter] would not have crashed during the Mars orbital insertion maneuver.
Mission and life-critical projects, or those for embedded devices, often have to obey the safety norms that care about software for safety-critical systems (e.g., ISO 61508 is a basic functional safety standard applicable to all industries, and ISO 26262 for automotive). As a result, their company policy often forbid third-party tooling that lacks official certification. Such certification requires a specification to be certified against, and those tools often do not have one. The risk and cost of self-certifying an Open Source project is too high for many as well.
Companies often have a policy that the software they use must obey all the rules MISRA provides. This is a common misconception, as many of those rules are intended to be deviated from. However, those deviations require rationale and documentation, which is also considered to be risky and expensive by many.
All of those reasons often prevent the usage of an Open Source product in a company, which is a huge issue, as those companies typically are natural users of physical quantities and units libraries.
Having the physical quantities and units library standardized would solve those issues for many customers, and would allow them to produce safer code for projects on which human life depends every single day.
Suppose vendors can’t use an Open Source library in a production
project for the above reasons. They are forced to write their own
abstractions by themselves. Besides being costly and time-consuming, it
also happens that writing a physical quantities and units library by
yourself is far from easy. Doing this is complex and complicated,
especially for engineers who are not experts in the domain. There are
many exceptional corner cases to cover that most developers do not even
realize before falling into a trap in production. On the other hand,
domain experts might find it difficult to put their knowledge into code
and create a correct implementation in C++. As a result, companies
either use really simple and unsafe numeric wrappers, or abandon the
effort entirely and just use built-in types, such as
float or
int, to
express quantity values, thus losing all semantic categorization. This
often leads to safety issues caused by accidentally using values
representing the wrong quantity or having an incorrect unit.
Many applications of a quantity and units library may need to operate on a combination of standard (e.g., SI) and domain-specific quantities and units. The complexity of developing domain-specific solutions highlights the value in being able to define new quantities and units that have all the expressivity and safety as those provided by the library.
Experience with writing ad hoc typed quantities without library
support that can be combined with or converted to std::chrono::duration
has shown the downside of bespoke solutions: If not all operations or
conversions are handled, users will need to leave the safety of typed
quantities to operate on primitive types.
The interfaces of the this library were designed with ease of extensibility in mind. Each definition of a dimension, quantity type, or unit typically takes only a single line of code. This is possible thanks to the extensive usage of C++20 class types as Non-Type Template Parameters (NTTP). For example, the following code presents how second (a unit of time in the [SI]) and hertz (a unit of frequency in the [SI]) can be defined:
inline constexpr struct second : named_unit<"s", kind_of<isq::time>> {} second;
inline constexpr struct hertz : named_unit<"Hz", 1 / second, kind_of<isq::frequency>> {} hertz;When people think about industries that could use physical quantities and unit libraries, they think of a few companies related to aerospace, autonomous cars, or embedded industries. That is all true, but there are many other potential users for such a library.
Here is a list of some less obvious candidates:
As we can see, the range of domains for such a library is vast and not limited to applications involving specifically physical units. Any software that involves measurements, or operations on counts of some standard or domain-specific quantities, could benefit from a zero-cost abstraction for operating on quantity values and their units. The library also provides affine space abstractions, which may prove useful in many applications.
Plenty of physical units libraries have been available to the public for many years. In 1998 Walter Brown provided an “Introduction to the SI Library of Unit-Based Computation” paper for the International Conference on Computing in High Energy Physics [CHEP’98]. It emphasizes the importance of strong types and static type-checking. After that, it describes a library modeling the [SI] to provide “strict compile-time type-checking without run-time overhead”.
It also states that at this time, “in numeric programming,
programmers make heavy, near-exclusive, use of a language’s native
numeric types (e.g.,
double)”.
Today, twenty-five years later, plenty of “Modern C++” production code
bases still use
double to
represent various quantities and units. It is high time to change
this.
Throughout the years, we have learned the best practices for handling specific cases in the domain. Various products may have different scopes and support different C++ versions. Still, taking that aside, they use really similar concepts, types, and operations under the hood. We know how to do those things already.
The authors of this paper developed and delivered multiple successful C++ libraries for this domain. Libraries developed by them have more than 90% of all the stars on GitHub in the field of physical units libraries for C++. The [mp-units] library, which is the base of this proposal, has the most number of stars in this list, making it the most popular project in the C++ industry.
The authors joined forces and are working together to propose the best quantities and units library we can get with the latest version of the C++ language. They spend their private time and efforts hoping that the ISO C++ Committee will be willing to include such a feature in the C++ standard library.
In Belfast 2019 the following polls were taken for [P1935R0] in LEWG:
POLL: We should promise more committee time to pursuing adding common units (such as SI, customary, etc) to the standard library, knowing that our time is scarce and this will leave less time for other work.
SF
|
WF
|
N
|
WA
|
SA
|
|---|---|---|---|---|
| 11 | 7 | 4 | 2 | 0 |
POLL: We should promise more committee time to pursuing a standard library framework for user defined units and unit systems, knowing that our time is scarce and this will leave less time for other work.
SF
|
WF
|
N
|
WA
|
SA
|
|---|---|---|---|---|
| 10 | 8 | 4 | 1 | 1 |
As a result of the above polls, Mateusz approached authors of all other popular actively maintained libraries. We formed a working group of experts and worked on a common unified proposal for a few years. This paper and the current implementation of the [mp-units] library is the result of those actions.
In Croydon 2026, this paper was reviewed by both SG18 (LEWG Incubator) and LEWG. LEWG took the following polls:
POLL: We acknowledge the complexity inherent to the domain of quantities and units and think this is a problem worth solving thoroughly in the standard library, following the direction presented in P3045R7.
SF
|
F
|
N
|
A
|
SA
|
|---|---|---|---|---|
| 22 | 13 | 1 | 0 | 0 |
Outcome: Strong consensus in favour
POLL: We support the direction presented as a technical solution (instantiating quantities with units, creating the hierarchy of kinds).
SF
|
F
|
N
|
A
|
SA
|
|---|---|---|---|---|
| 16 | 22 | 0 | 0 | 0 |
Outcome: Strong consensus in favour
POLL: We see the value in covering use cases in fine granularity (different quantities of the same kind, such as width and height, or difference between kinetic and potential energy).
SF
|
F
|
N
|
A
|
SA
|
|---|---|---|---|---|
| 22 | 12 | 3 | 0 | 0 |
Outcome: Strong consensus in favour
POLL: We see the value of standardizing these parts of the framework: Core library (quantity, symbolic expressions, dimensions, units, references and concepts), Quantity kinds (support quantities of the same dimension), Quantities of the same kind (width, height, etc.).
SF
|
F
|
N
|
A
|
SA
|
|---|---|---|---|---|
| 27 | 9 | 0 | 0 | 0 |
Outcome: Strong consensus in favour
POLL: We see the value of standardizing Affine space (quantity_point, point origin, and concepts for them).
SF
|
F
|
N
|
A
|
SA
|
|---|---|---|---|---|
| 24 | 8 | 1 | 0 | 0 |
Outcome: Strong consensus in favor
POLL: We see the value of standardizing Text output (for quantity, units, and dimensions).
SF
|
F
|
N
|
A
|
SA
|
|---|---|---|---|---|
| 15 | 14 | 5 | 0 | 0 |
Outcome: Strong consensus in favour
These results demonstrate strong committee support for the comprehensive approach to quantities and units presented in this paper, including the more advanced features like affine space abstractions and fine-grained quantity specifications.
In this chapter, we are going to review typical safety issues related to physical quantities and units in the C++ code when a proper library is not used. Even though all the examples come from the Open Source projects, expensive revenue-generating production source code often is similar.
doubleIt turns out that in the C++ software, most of our calculations in
the physical quantities and units domain are handled with fundamental
types like
double. Code
like below is a typical example here:
double GlidePolar::MacCreadyAltitude(double MCREADY,
double Distance,
const double Bearing,
const double WindSpeed,
const double WindBearing,
double *BestCruiseTrack,
double *VMacCready,
const bool isFinalGlide,
double *TimeToGo,
const double AltitudeAboveTarget=1.0e6,
const double cruise_efficiency=1.0,
const double TaskAltDiff=-1.0e6);There are several problems with such an approach: The abundance of
double
parameters makes it easy to accidentally switch values and there is no
way of noticing such a mistake at compile-time. The code is not
self-documenting in what units the parameters are expected. Is
Distance in meters or kilometers? Is
WindSpeed in meters per second or
knots? Different code bases choose different ways to encode this
information, which may be internally inconsistent. A strong type system
would help answer these questions at the time the interface is written,
and the compiler would verify it at compile-time.
There are a lot of constants and conversion factors involved in the quantity equations. Source code responsible for such computations is often trashed with magic numbers:
double AirDensity(double hr, double temp, double abs_press)
{
return (1/(287.06*(temp+273.15)))*(abs_press - 230.617 * hr * exp((17.5043*temp)/(241.2+temp)));
}Apart from the obvious readability issues, such code is hard to
maintain, and it needs a lot of domain knowledge on the developer’s
side. While it would be easy to replace these numbers with named
constants, the question of which unit the constant is in remains. Is
287.06 in
pounds per square inch (psi) or millibars (mbar)?
The lack of automated unit conversions often results in handwritten conversion functions or macros that are spread everywhere among the code base:
#ifndef PI
static const double PI = (4*atan(1));
#endif
#define EARTH_DIAMETER 12733426.0 // Diameter of earth in meters
#define SQUARED_EARTH_DIAMETER 162140137697476.0 // Diameter of earth in meters (EARTH_DIAMETER*EARTH_DIAMETER)
#ifndef DEG_TO_RAD
#define DEG_TO_RAD (PI / 180)
#define RAD_TO_DEG (180 / PI)
#endif
#define NAUTICALMILESTOMETRES (double)1851.96
#define KNOTSTOMETRESSECONDS (double)0.5144
#define TOKNOTS (double)1.944
#define TOFEETPERMINUTE (double)196.9
#define TOMPH (double)2.237
#define TOKPH (double)3.6
// meters to.. conversion
#define TONAUTICALMILES (1.0 / 1852.0)
#define TOMILES (1.0 / 1609.344)
#define TOKILOMETER (0.001)
#define TOFEET (1.0 / 0.3048)
#define TOMETER (1.0)Again, the question of which unit the constant is in remains. Without
looking at the code, it is impossible to tell from which unit
TOMETER converts. Also, macros have
the problem that they are not scoped to a namespace and thus can easily
clash with other macros or functions, especially if they have such
common names like PI or
RAD_TO_DEG. A quick search through
open source C++ code bases reveals that, for example, the
RAD_TO_DEG macro is defined in a
multitude of different ways – sometimes even within the same
repository:
#define RAD_TO_DEG (180 / PI)
#define RAD_TO_DEG 57.2957795131
#define RAD_TO_DEG ( radians ) ((radians ) * 180.0 / M_PI)
#define RAD_TO_DEG 57.2957805f
...Example search across multiple repositories
Multiple redefinitions in the same repository
Another safety issue occurring here is the fact that macro values can be deliberately tainted by compiler settings at built time and can acquire values that are not present in the source code. Human reviews won’t catch such issues.
Also, most of the macros do not follow best practices. Often,
necessary parentheses are missing, processing in a preprocessor ends up
with redundant casts, or some compile-time constants use too many digits
for a value to be exact for a specific type (e.g.,
float).
If we not only lack strong types to isolate the abstractions from each other, but also lack discipline to keep our code consistent, we end up in an awful place:
void DistanceBearing(double lat1, double lon1,
double lat2, double lon2,
double *Distance, double *Bearing);
double DoubleDistance(double lat1, double lon1,
double lat2, double lon2,
double lat3, double lon3);
void FindLatitudeLongitude(double Lat, double Lon,
double Bearing, double Distance,
double *lat_out, double *lon_out);
double CrossTrackError(double lon1, double lat1,
double lon2, double lat2,
double lon3, double lat3,
double *lon4, double *lat4);
double ProjectedDistance(double lon1, double lat1,
double lon2, double lat2,
double lon3, double lat3,
double *xtd, double *crs);Users can easily make errors if the interface designers are not
consistent in ordering parameters. It is really hard to remember which
function takes latitude or Bearing
first and when a latitude or
Distance is in the front.
The previous points mean that the fundamental types can’t be leveraged to model the different concepts of quantities and units frameworks. There is no shared vocabulary between different libraries. User-facing APIs use ad-hoc conventions. Even internal interfaces are inconsistent between themselves.
Arithmetic types such as
int and
double are
used to model different concepts. They are used to represent any
abstraction (be it a magnitude, difference, point, or kind) of any
quantity type of any unit. These are weak types that make up
weakly-typed interfaces. The resulting interfaces and implementations
built with these types easily allow mixing up parameters and using
operations that are not part of the represented quantity.
The library facilities that we plan to propose in the upcoming papers is designed with the following goals in mind.
The most important property of any such a library is the safety it brings to C++ projects. The correct handling of physical quantities, units, and numerical values should be verifiable both by the compiler and by humans with manual inspection of each individual line.
In some cases, we are even eager to prioritize safe interfaces over the general usability experience (e.g., getters of the underlying raw numerical value will always require a unit in which the value should be returned in, which results in more typing and is sometimes redundant).
More information on this subject can be found in [Safety features].
The library should be as fast or even faster than working with
fundamental types. There should be no runtime overhead, and no space
size overhead should be needed to implement higher-level abstractions.
In practice, the [mp-units] implementation compiles to
identical or faster assembly as equivalent code using raw
double
arithmetic — this can be verified via the Compiler Explorer links
provided in the Usage examples
chapter.
The primary purpose of the library is to generate compile-time errors. If users did not introduce any bugs in the manual handling of quantities and units, the library would be of little use. This is why the library is optimized for readable compilation errors and great debugging experience.
The library is easy to use and flexible. The interfaces are straight-forward and safe by default. Users should be able to easily express any quantity and unit, which requires them to compose.
The above constraints imply the usage of special implementation techniques. The library will not only provide types, but also compile-time known values that will enable users to write easy to understand and efficient equations on quantities and units.
There are plenty of expectations from different parties regarding such a library. It should support at least:
Additionally, it would be good to also support the following features:
The library’s core framework does not assume the usage of any systems of quantities or units. It is fully generic and allow defining any system abstraction on top of it.
Most entities in the library can be defined with a single line of code without preprocessor macros. Users can easily extend provided systems with custom dimensions, quantities, and units.
The set of entities required for standardization should be limited to the bare minimum.
Most of the entities in systems definitions should be possible to implement with a single line of code.
Derived units do not need separate library types. Instead, they can
be obtained through the composition of predefined named units. Units
should not be associated with User-Defined Literals (UDLs), as it is the
case with std::chrono::duration.
UDLs do not compose, have very limited scope and functionality, and are
expensive to standardize.
The user interface should have no preprocessor macros.
It should be possible for most proposed features (besides the text output) to be freestanding.
This chapter provides a brief introduction to the quantities and units domain. Please refer to [ISO/IEC 80000] and [SI] for more details.
Note: A more detailed graph of the framework’s entities can be found in the Framework entities chapter.
Dimension specifies the dependence of a quantity on the base quantities of a particular system of quantities. It is represented as a product of powers of factors corresponding to the base quantities, omitting any numerical factor.
Even though ISO does not officially define these, we find the below terms useful when discussing the domain and its C++ implementation:
As stated above, ISO does not mention a “base dimension” term. Nevertheless, it treats dimensions of base quantities in a special way by:
For example:
Dimension alone is insufficient to describe a quantity. [ISO/IEC 80000] provides hundreds of named quantity types—much more than the named units in [SI].
[ISO/IEC 80000] defines kind of quantity as an aspect common to mutually comparable quantities. Two or more quantities cannot be added or subtracted unless they belong to the same category of mutually comparable quantities.
Quantities might be:
ISO specifies quantities of dimension one (dimensionless quantities) where all exponents of base quantities are zero. These typically represent ratios of quantities of the same dimension or counts.
[ISO/IEC 80000] defines quantity value as number and reference together expressing magnitude of a quantity, where the reference can be a measurement unit, measurement procedure, reference material, or combination thereof.
Measurement units are designated by assigned names and symbols. Units of quantities with the same dimension may share names and symbols even when quantities differ in kind (e.g., joule per kelvin for both heat capacity and entropy). However, some units are restricted to specific kinds (e.g., hertz for frequency, becquerel for radioactive activity, both equivalent to 1/s).
Dimensionless quantities have units that are numbers, sometimes with special names (radian, steradian, decibel) or expressed as ratios (millimole per mole: \(10^{−3}\), microgram per kilogram: \(10^{−9}\)).
[ISO/IEC 80000] defines a quantity as “property of a phenomenon, body, or substance, where the property has a magnitude that can be expressed as a number and a reference.”
This means a quantity abstraction should store a numerical value and a reference (typically a unit). Common practice embeds the reference into the C++ type to avoid runtime storage overhead.
It is also worth mentioning here that the distinction between a quantity and a quantity type is not clearly defined in [ISO/IEC 80000]. [ISO/IEC 80000] even explicitly states:
It is customary to use the same term, “quantity”, to refer to both general quantities, such as length, mass, etc., and their instances, such as given lengths, given masses, etc. Accordingly, we are used to saying both that length is a quantity and that a given length is a quantity, by maintaining the specification – “general quantity, \(Q\)” or “individual quantity, \(Q_a\)” – implicit and exploiting the linguistic context to remove the ambiguity.
To prevent such ambiguities in this document, we will consistently use the term:
It is also worth mentioning here that [ISO/IEC 80000] does not distinguish between point and vector/interval quantities of The affine space.
This chapter presents the library’s core abstractions and design
choices. The quantity class template
represents displacement vectors in an affine space, while
quantity_point represents points.
Generic interfaces enable efficient, type-safe code without
unit-specific functions.
Note for readers: Most application developers need only understand Quantity construction and basic operations (arithmetic, conversions, formatting). Generic interfaces are primarily for library developers designing reusable libraries. More advanced topics like affine space and quantity specifications are covered later for domain specialists and framework developers.
More details about the design, rationale for it, and alternative syntaxes discussions can be found in the Design details and rationale chapter.
The quantity class template takes
a reference and representation type:
template<Reference auto R,
RepresentationOf<get_quantity_spec(R)> Rep = double>
class quantity;quantity is best understood as a
numerical strong type wrapper: it takes any number-like
representation type and enriches it with compile-time metadata — a
quantity specification and a measurement unit — collectively called the
quantity reference — that the type system uses to
enforce correctness. The runtime cost is zero; all safety guarantees are
resolved at compile time.
This design is not limited to physical computation. Any countable or
measurable domain is a valid target: file sizes, pixel counts, angular
positions, currency amounts, database row counts, audio sample offsets,
and more. If a value can be added to another value of the same kind,
multiplied by a dimensionless factor, or meaningfully converted to a
different scale, quantity can model
it. The library is a general-purpose compile-time safety layer for
numeric domain modelling, of which SI units are simply the most
prominent example.
If we want to set a value for a quantity, we always have to provide a number and a unit:
quantity<si::metre, int> q{42, si::metre};In case a quantity class template should use exactly the same unit and a representation type as provided in the initializer, it is recommended to use CTAD:
quantity q{42, si::metre};The [SI] says:
The value of the quantity is the product of the number and the unit. The space between the number and the unit is regarded as a multiplication sign (just as a space between units implies multiplication).
Following the above, the value of a quantity can also be created by multiplying a number with a predefined unit:
quantity q = 42 * si::metre;The above creates an instance of quantity<si::metre(), int>.
It is worth noting here that the syntax with the reversed order of
arguments is invalid and will not compile (e.g., we can’t write si::metre * 42).
The same can be obtained using an optional unit symbol:
using namespace si::unit_symbols;
quantity q = 42 * m;Unit symbols introduce a lot of short identifiers into the current
scope, which is why they are opt-in. A user has to explicitly “import”
them from a dedicated unit_symbols
namespace.
[SI] specifies 7 base and 22 coherent derived units with special names. Additionally, it specifies 24 prefixes. There are also non-SI units accepted for use with SI. Some of them are really popular, for example, minute, hour, day, degree, litre, hectare, tonne. All of those entities compose to allow the creation of a vast number of various derived units.
For example, we can create a quantity of speed with either:
quantity speed1 = 60 * si::kilo<si::metre> / non_si::hour;
quantity speed2 = 60 * km / h;The library is optimized to generate short and easy-to-understand
types that highly improve the analysis of compile-time errors and
debugging experience. All of the above definitions will create an
instance of the type quantity<derived_unit<si::kilo_<si::metre>, per<non_si::hour>>{}, int>>.
As we can see, the type generation is optimized to be easily understood
even by non-experts in the domain. The library tries to keep the type’s
readability as close to English as possible.
To find more discussion on a quantity creation syntax please refer to the following chapters:
explicit
is not explicit enough,Unit-specific function interfaces introduce several problems:
quantity<km / h> avg_speed(quantity<km> distance, quantity<h> duration)
{
return distance / duration;
}Using this function:
quantity<km / h> s1 = avg_speed(220 * km, 2 * h);
quantity<mi / h> s2 = avg_speed(140 * mi, 2 * h);
quantity<m / s> s3 = avg_speed(20 * m, 2 * s);Problems:
value_cast or
force_in, which can produce
incorrect results (e.g., division by zero).Generic code using quantity concepts eliminates these issues:
auto avg_speed(QuantityOf<isq::length> auto distance,
QuantityOf<isq::time> auto duration)
{
return isq::speed(distance / duration);
}This ensures arguments are implicitly convertible to the required quantity types while allowing the compiler to generate optimal code without conversions. Integral types can be safely used, improving performance.
Constraining return types provides documentation and verification:
QuantityOf<isq::speed> auto avg_speed(QuantityOf<isq::length> auto distance,
QuantityOf<isq::time> auto duration)
{
return isq::speed(distance / duration);
}Benefits:
When using generic interfaces, constrain variables with concepts to document intent and verify correctness:
QuantityOf<isq::speed> auto s1 = avg_speed(220 * km, 2 * h);
QuantityOf<isq::speed> auto s2 = avg_speed(140 * mi, 2 * h);
QuantityOf<isq::speed> auto s3 = avg_speed(20 * m, 2 * s);This serves as documentation and a unit test for the returned type.
The affine space distinguishes between:
The displacement vector described here is specific to the affine space theory and is not the same thing as the quantity of a vector character that we discuss later (although, in some cases, those terms may overlap).
In the following subchapters, we will often refer to displacement vectors simply as vectors for brevity.
Valid operations:
It is not possible to:
quantityThe quantity type models
displacement vectors. Construction options include:
42 * m,
100 * km / hdelta<Reference>
constructor: delta<deg_C>(3),
delta<isq::height[m]>(42)quantity{42, si::metre}The multiply syntax is disabled for units with inherent point origins
(temperature units). Rationale is in delta
and point creation helpers.
quantity_point and
PointOriginThe quantity_point class template
represents points:
template<Reference auto R,
PointOriginFor<get_quantity_spec(R)> auto PO = default_point_origin(R),
RepresentationOf<get_quantity_spec(R)> Rep = double>
class quantity_point;The PO parameter specifies the
measurement origin. By default, default_point_origin(R)
provides:
natural_point_origin<QuantitySpec>
for other quantities.Construction requires explicit conversion or the
point helper:
quantity_point qp1(42 * m); // explicit conversion
quantity_point qp2 = point<m>(42); // construction helper
quantity_point qp3 = point<deg_C>(21); // temperature pointMultiply syntax is disabled to prevent confusion between vectors and points.
natural_point_origin<QuantitySpec>natural_point_origin<QuantitySpec>
provides a default origin for domains with a well-established, unique
zeroth point, eliminating boilerplate:
quantity_point<isq::distance[si::metre]> qp1(100 * m);
quantity_point<isq::distance[si::metre]> qp2 = point<m>(120);
assert(qp2 - qp1 == 20 * m);
assert(qp1.quantity_from_unit_zero() == 100 * m);
// auto res = qp1 + qp2; // Compile-time errorKey design considerations:
zeroth_point_origin.natural_point_origin makes quantity
points compatible when their quantity types are compatible (e.g., isq::distance and
isq::height
points can be subtracted), which may be surprising but enables ergonomic
usage for common cases.Absolute point origins establish isolated, independent spaces where points are incompatible even with the same quantity type:
inline constexpr struct origin : absolute_point_origin<isq::distance> {} origin;
// quantity_point<si::metre, origin> qp1{100 * m}; // Compile-time error
quantity_point<si::metre, origin> qp1 = origin + 100 * m;
quantity_point qp2{120 * m, origin}; // alternative syntax
assert(qp1.quantity_from(origin) == 100 * m);
assert(qp1 - origin == 100 * m);
assert(qp2 - qp1 == 20 * m);
assert(origin - qp1 == -100 * m);
// assert(origin - origin == 0 * m); // Compile-time errorDesign rationale: Safety requires explicit
construction with both origin and displacement vector. Direct
construction from quantities is prevented. Subtracting two
absolute_point_origin instances is
forbidden because they lack unit information needed to determine the
resulting quantity type.
Absolute point origins enable multiple independent coordinate systems within the same quantity type:
inline constexpr struct origin1 : absolute_point_origin<isq::distance> {} origin1;
inline constexpr struct origin2 : absolute_point_origin<isq::distance> {} origin2;
quantity_point qp1 = origin1 + 100 * m;
quantity_point qp2 = origin2 + 120 * m;
assert(qp1 - origin1 == 100 * m);
// assert(qp2 - qp1 == 20 * m); // Compile-time error: incompatible origins
// assert(qp1 - origin2 == 100 * m); // Compile-time error: wrong originRelative point origins support common scales with multiple reference points that remain compatible (temperatures, timestamps, altitudes):
inline constexpr struct A : absolute_point_origin<isq::distance> {} A;
inline constexpr struct B : relative_point_origin<A + 10 * m> {} B;
inline constexpr struct C : relative_point_origin<B + 10 * m> {} C;
inline constexpr struct D : relative_point_origin<A + 30 * m> {} D;
quantity_point qp1 = C + 100 * m;
quantity_point qp2 = D + 120 * m;
assert(qp2 - qp1 == 30 * m); // Compatible: both relative to A
assert(qp1.quantity_from(C) == 100 * m);
assert(qp1.quantity_from(A) == 120 * m);
assert(B - A == 10 * m); // Can subtract relative from absolute
assert(C - B == 10 * m); // Can subtract relative origins
// assert(A - A == 0 * m); // Compile-time errorDesign feature: Unlike absolute origins, relative origins can be subtracted from each other or from their absolute base.
The same point can be represented with displacement vectors from various origins:
quantity_point<si::metre, C> qp2C = qp2; // converting constructor
quantity_point qp2B = qp2.point_for(B); // conversion interface
assert(qp2 == qp2C); // Same point, different representation
assert(qp2 == qp2B);Design constraint: Conversions are only allowed
between origins sharing the same
absolute_point_origin base. There is
no way to express relationships between distinct absolute origins—custom
conversion functions are required for such cases.
Temperature is a canonical example of relative point origins with stacked hierarchies:
namespace si {
inline constexpr struct absolute_zero : absolute_point_origin<isq::thermodynamic_temperature> {} absolute_zero;
inline constexpr struct ice_point : relative_point_origin<point<milli<kelvin>>(273'150)> {} ice_point;
}
namespace usc {
inline constexpr struct zeroth_degree_Fahrenheit :
relative_point_origin<point<mag_ratio<5, 9> * si::degree_Celsius>(-32)> {} zeroth_degree_Fahrenheit;
}Design feature: Origins stack hierarchically (°F → °C → K), and units embed their natural origins:
namespace si {
inline constexpr struct kelvin : named_unit<"K", kind_of<isq::thermodynamic_temperature>, zeroth_kelvin> {} kelvin;
inline constexpr struct degree_Celsius : named_unit<{u8"℃", "`C"}, kelvin, zeroth_degree_Celsius> {} degree_Celsius;
}Construction syntax flexibility (all produce the same type):
quantity_point<si::degree_Celsius, si::zeroth_degree_Celsius> q1 = si::zeroth_degree_Celsius + delta<deg_C>(20.5);
quantity_point q2{delta<deg_C>(20.5)};
quantity_point q3 = point<deg_C>(20.5);Custom temperature references enable domain-specific applications:
constexpr struct room_reference_temp : relative_point_origin<point<deg_C>(21)> {} room_reference_temp;
using room_temp = quantity_point<isq::Celsius_temperature[deg_C], room_reference_temp>;
constexpr auto step_delta = delta<isq::Celsius_temperature[deg_C]>(0.5);
constexpr int number_of_steps = 6;
room_temp room_ref{};
room_temp room_low = room_ref - number_of_steps * step_delta;
room_temp room_high = room_ref + number_of_steps * step_delta;
std::println("Room reference temperature: {} ({}, {::N[.2f]})\n",
room_ref.quantity_from_unit_zero(),
room_ref.in(deg_F).quantity_from_unit_zero(),
room_ref.in(K).quantity_from_unit_zero());
std::println("| {:<18} | {:^18} | {:^18} | {:^18} |",
"Temperature delta", "Room reference", "Ice point", "Absolute zero");
std::println("|{0:=^20}|{0:=^20}|{0:=^20}|{0:=^20}|", "");
auto print_temp = [&](std::string_view label, auto v) {
std::println("| {:<18} | {:^18} | {:^18} | {:^18:N[.2f]} |", label,
v - room_reference_temp, (v - si::ice_point).in(deg_C), (v - si::absolute_zero).in(deg_C));
};
print_temp("Lowest", room_low);
print_temp("Default", room_ref);
print_temp("Highest", room_high);The above prints:
Room reference temperature: 21 ℃ (69.8 ℉, 294.15 K)
| Temperature delta | Room reference | Ice point | Absolute zero |
|====================|====================|====================|====================|
| Lowest | -3 ℃ | 18 ℃ | 291.15 ℃ |
| Default | 0 ℃ | 21 ℃ | 294.15 ℃ |
| Highest | 3 ℃ | 24 ℃ | 297.15 ℃ |
More about temperatures can be found in the [Potential surprises while working with temperatures] chapter.
The library should work with any representation type for:
By default, floating-point and integral types (except
bool) are
treated as real scalars.
Every quantity has a
representation type that stores the numerical value.
The library works seamlessly with fundamental arithmetic types (except
bool) and
std::complex,
but custom representation types can also be used to model
domain-specific requirements—such as range-validated values, vectors, or
specialized numeric types.
The representation type determines what mathematical operations are available and how the quantity behaves in calculations. The library verifies at compile time that the representation type has the capabilities required for the quantity’s character.
To be used as a representation type, a type must satisfy the
RepresentationOf concept. The
library supports different types of representations corresponding to
different quantity characters.
Why verify representation capabilities? The same unit can represent fundamentally different physical concepts requiring different mathematical operations. For example:
The library tracks character in the quantity specification (what the quantity represents) and verifies that the representation type provides the required capabilities. This dual approach provides compile-time type safety for the mathematical nature of physical quantities—preventing, for example, using a scalar type where vector operations like cross product are needed.
The following table summarizes the requirements for different representation characters:
Requirement
|
Real Scalar
|
Complex Scalar
|
Vector
|
Tensor
|
|---|---|---|---|---|
| Copyable | ✅ | ✅ | ✅ | ✅ |
Addition/subtraction
(+,
-, unary
-) |
✅ | ✅ | ✅ | ✅ |
MagnitudeScalable
(unit-conversion) |
✅ | ✅ | ✅ | ✅ |
Self-scalable
(T * T,
T / T) |
✅ | ✅ | - | - |
Equality comparable
(==) |
✅ | ✅ | ✅ | ✅ |
Totally ordered
(<,
>,
<=,
>=) |
✅ | - | - | - |
| Not a quantity type itself | ✅ | ✅ | ✅ | ✅ |
| Construction | - | T{real, imag} |
- | - |
| Required CPOs | - | mp_units::real(),
mp_units::imag(),
mp_units::modulus() |
mp_units::norm() |
mp_units::norm() |
| Opt-out mechanism | disable_real<T> |
- | - | - |
| Examples | int,
double,
long double |
std::complex<double> |
Eigen::Vector3d,
cartesian_vector<double>,
int,
double |
Eigen::Matrix3d,
int,
double (for
scalar measures) |
All representation types must be weakly regular,
which means they satisfy the
std::regular
concept except for the default-constructibility requirement.
Specifically, they must be:
std::copyable)std::equality_comparable)This ensures that representation types have value semantics suitable for use in quantities. Default construction is not required, allowing types like range-validated representations that may not have a meaningful default value.
Construction
Complex scalars must be constructible from real and
imaginary parts: T{real_value, imag_value}.
This is essential for operations that combine real-valued quantities
into complex results. For example, combining active power and
reactive power into complex power:
quantity active = isq::active_power(100.0 * W);
quantity reactive = isq::reactive_power(50.0 * W);
// Library needs to construct: std::complex<double>{active.numerical_value(),
// reactive.numerical_value()}Total Ordering
Well-designed complex-like types do not provide total ordering (operator<,
etc.) since there is no natural ordering for complex numbers. If a
complex-like type does provide ordering operators (e.g., for use in
containers), use the disable_real
opt-out mechanism:
template<>
constexpr bool mp_units::disable_real<my_complex_type> = true;Alternatively, the library could explicitly check for the absence of
mp_units::real()
and mp_units::imag()
to distinguish real from complex scalars — a design choice that may be
refined based on standardization discussions.
The different names reflect domain conventions:
modulus() is
traditional complex analysis terminology, while
norm() is
standard linear algebra terminology used across industry (Eigen, NumPy,
MATLAB, Armadillo). The library follows these established conventions
rather than imposing a single unified name (e.g.,
abs()).
Additionally,
magnitude()
is provided as an alias for
norm() for
compatibility with physics terminology.
Arithmetic types like
int and
double
intentionally satisfy requirements for multiple characters — real scalar
(primary use), 1-dimensional vector, and scalar tensor measures like von
Mises stress. Type safety comes from
quantity_character matching in the
quantity specification, not from mutually exclusive representation
concepts:
// All valid uses of double:
quantity m = isq::mass(5.0 * kg); // Scalar
quantity v = isq::velocity(10.0 * m/s); // 1D vector
quantity sigma = isq::stress(100.0 * Pa); // Scalar tensor measureMost engineering extracts scalar measures from tensor fields rather than working with full 3×3 matrix representations — von Mises stress, principal stresses, shear components, hydrostatic stress — which is why arithmetic types cover the tensor character in practice.
The library provides several customization mechanisms for representation types. These fall into two categories: Character determination (what kind of representation type you have) and Behavior and values (how the library interacts with your type).
The library uses several CPOs to support different representation types. Providing these CPOs determines the character of the representation type. Each CPO checks for implementations in the following priority order:
mp_units::real(c)
- Returns the real part of a complex number:
c.real()
member functionreal(c)
free function found via ADLmp_units::imag(c)
- Returns the imaginary part of a complex number:
c.imag()
member functionimag(c)
free function found via ADLmp_units::modulus(c)
- Returns the magnitude of a complex number:
c.modulus()
member functionmodulus(c)
free function found via ADLc.abs()
member functionabs(c)
free function found via ADLmp_units::norm(v)
- Returns the norm (magnitude) of a vector or tensor as a scalar:
v.norm()
member functionnorm(v)
free function found via ADLstd::abs(v)v.abs()
member functionabs(v)
free function found via ADLmp_units::magnitude(v)
- Returns the magnitude of a vector as a scalar:
mp_units::norm(v)
(provided for compatibility with physics terminology)For
modulus(),
abs() is
checked as a fallback for compatibility with
std::complex
and similar types that use that name. For
norm(),
abs()
enables arithmetic types to serve as 1-dimensional vectors and scalar
tensor measures, which accurately reflects engineering practice where
most calculations use scalar values rather than full vector/tensor
representations. —
disable_real<T>A specializable variable template to opt out a type from being treated as a real scalar:
template<typename T>
constexpr bool mp_units::disable_real = false;Specializing to
true
prevents a type from being classified as real scalar character even if
it satisfies all syntactic requirements. The library uses this
internally to exclude
bool, which
is totally ordered and arithmetic but meaningless as a quantity:
template<>
constexpr bool mp_units::disable_real<my_type> = true;representation_underlying_type<T>representation_underlying_type<T>
is the extension point for exposing the underlying arithmetic or element
type of a representation to the library. It drives the scaling factor
type and the treat_as_floating_point
check:
template<typename T>
struct mp_units::representation_underlying_type; // primary — empty
template<typename T>
using mp_units::representation_underlying_type_t = representation_underlying_type<T>::type;The library provides partial specializations that detect the underlying type in order:
T::value_type or
T::element_type
member type (cv-qualification stripped)std::underlying_type_t<T>
for scoped enumerations (unscoped enumerations are excluded — they
already implicitly convert to their underlying type)T itself as a fallbackIf both value_type and
element_type are present with
differing underlying types, the trait is empty and the library treats
T as a leaf — provide only
value_type unless there is a
specific reason to expose both (e.g., satisfying iterator concepts), in
which case ensure they name the same underlying type.
A value_type member is the
preferred form for types under the user’s control:
template<typename T>
class my_wrapper {
public:
using value_type = T;
// ...
};When the source of a type cannot be modified, the trait may be specialized directly:
// MyFloat wraps long double internally
template<>
struct mp_units::representation_underlying_type<MyFloat> {
using type = long double;
};[ Note: std::indirectly_readable_traits
was intentionally not reused: that standard trait answers “what does
*t yield?”
and is the extension point for iterators and smart pointers —
specializing it for a non-iterator type is a semantic misuse. —
end note ]
The library scales a representation value by calling value * factor and
value / factor,
where factor is of type representation_underlying_type_t<T>
(or a wider integer type for the rational integer path — see [How
Scaling Works?] for details). A type may additionally provide operator*(T, UnitMagnitude)
to receive the full compile-time unit magnitude; when present, this
operator is called first and the factor-based operators
serve as a fallback. The magnitude-aware operator may return a
different type — see Magnitude-aware scaling for the full
pattern.
These operators are found via ADL. Hidden friends are the preferred form for types under the user’s control; non-member operators placed in the type’s namespace serve the same role for third-party types:
template<typename T>
class my_wrapper {
T value_;
public:
using value_type = T;
friend constexpr my_wrapper operator*(my_wrapper v, T factor) { return my_wrapper{v.value_ * factor}; }
friend constexpr my_wrapper operator/(my_wrapper v, T factor) { return my_wrapper{v.value_ / factor}; }
// Optional: magnitude-aware scaling (return type may differ from my_wrapper)
// template<mp_units::UnitMagnitude M>
// friend constexpr auto operator*(const my_wrapper& v, M m) { /* ... */ }
};treat_as_floating_point<Rep>A specializable variable template that tells the library whether a type should be treated as floating-point for the purpose of allowing implicit conversions:
template<typename Rep>
constexpr bool mp_units::treat_as_floating_point = /* implementation-defined */;By default, the value is determined by applying std::chrono::treat_as_floating_point_v
(hosted) or std::is_floating_point_v
(freestanding) to the recursively-unwrapped underlying type of
Rep. When
true,
implicit conversions are enabled; otherwise an explicit
value_cast is required (see Value conversions). A specialization is
needed when automatic detection yields an incorrect result:
template<>
constexpr bool mp_units::treat_as_floating_point<my_fixed_point_type> = true;implicitly_scalable<FromUnit, FromRep, ToUnit, ToRep>A specializable variable template that controls
whether a conversion from quantity<FromUnit, FromRep>
to quantity<ToUnit, ToRep>
is implicit or requires an explicit cast via
value_cast/force_in.
It is the policy layer built on top of
treat_as_floating_point: the default
formula derives the implicit-conversion decision from it, and a
specialization overrides that decision for types where the derived rule
is incorrect:
template<auto FromUnit, typename FromRep, auto ToUnit, typename ToRep>
constexpr bool mp_units::implicitly_scalable =
treat_as_floating_point<ToRep> ||
(!treat_as_floating_point<FromRep> && is_integral_scaling(FromUnit, ToUnit));mp_units::is_integral_scaling(from, to)
is a
consteval
predicate that can also be used in user specializations to distinguish
the integral-factor case
(e.g. m → mm (×1000)) from
fractional ones (e.g. mm → m
(÷1000), ft → m,
deg → rad).
The default follows the precedent of std::chrono::duration:
conversions to a floating-point representation are always implicit,
conversions between integer representations are implicit only when the
unit ratio is an integer multiplier (exact, no truncation), and all
other cases require an explicit cast.
For example, a decimal fixed-point type that represents fractional ratios exactly can permit all unit conversions implicitly:
template<auto FromUnit, auto ToUnit>
constexpr bool mp_units::implicitly_scalable<FromUnit, safe_decimal, ToUnit, safe_decimal> = true;When precision is asymmetric between two types, the specialization can be directional:
template<auto FromUnit, auto ToUnit>
constexpr bool mp_units::implicitly_scalable<FromUnit, double, ToUnit, my_decimal> = true;
template<auto FromUnit, auto ToUnit>
constexpr bool mp_units::implicitly_scalable<FromUnit, my_decimal, ToUnit, double> = false;mp_units::is_integral_scaling
may be reused in a specialization to distinguish integral from
fractional unit ratios. See Value
conversions for details.
representation_values<Rep>A specializable class template that provides the special values used
by quantity::zero(),
quantity::min(),
quantity::max(),
mathematical rounding operations, and division-by-zero checks:
template<typename Rep>
struct mp_units::representation_values {
static constexpr Rep zero() noexcept;
static constexpr Rep one() noexcept;
static constexpr Rep min() noexcept;
static constexpr Rep max() noexcept;
};In hosted environments the primary specialization inherits
zero(),
min(), and
max() from
std::chrono::duration_values<Rep>;
one() is
always defined in the struct itself, constrained to std::constructible_from<Rep, int>.
In freestanding environments all four methods are defined directly, each
guarded by its own
requires
clause:
zero() and
one()
require std::constructible_from<Rep, int>;
min()
requires std::numeric_limits<Rep>::is_specialized
and that std::numeric_limits<Rep>::lowest()
returns Rep;
max()
requires the same plus std::numeric_limits<Rep>::max()
returning Rep. An explicit
specialization is required for types that cannot satisfy those
constraints or that need non-standard special values:
template<typename T>
struct mp_units::representation_values<my_custom_type<T>> {
static constexpr my_custom_type<T> zero() noexcept
{ return my_custom_type<T>{T{0}}; }
static constexpr my_custom_type<T> one() noexcept
{ return my_custom_type<T>{T{1}}; }
static constexpr my_custom_type<T> min() noexcept
{ return my_custom_type<T>{std::numeric_limits<T>::lowest()}; }
static constexpr my_custom_type<T> max() noexcept
{ return my_custom_type<T>{std::numeric_limits<T>::max()}; }
};Every representation type must be unit-conversion
scalable — the library must be able to apply a unit magnitude
ratio to it internally. This is captured by the
MagnitudeScalable concept, which
directly names the three built-in scaling paths:
concept MagnitudeScalable =
WeaklyRegular<T> && (UsesMagnitudeAwareScaling<T> || UsesFloatingPointScaling<T> || UsesIntegerScaling<T>);UsesMagnitudeAwareScaling is
satisfied by any type that provides operator*(T, UnitMagnitude)
— checked first by the scaling engine, before the two built-in numeric
paths. The full pattern is described in Magnitude-aware scaling:
concept UsesMagnitudeAwareScaling = requires(const T& v) { v * mag<1>; };UsesFloatingPointScaling matches
any type — or container thereof — whose underlying type satisfies
treat_as_floating_point, is
constructible from long double
(the precision at which magnitude constants are evaluated), and supports
operator*
and operator/
with that underlying type, returning a weakly-regular result:
concept UsesFloatingPointScaling =
(treat_as_floating_point<T> || treat_as_floating_point<representation_underlying_type_t<T>>) &&
std::constructible_from<representation_underlying_type_t<T>, long double> &&
requires(T value, representation_underlying_type_t<T> f) {
{ value * f } -> WeaklyRegular;
{ value / f } -> WeaklyRegular;
};UsesIntegerScaling matches any
type whose underlying type satisfies detail::integral
(the scaling engine uses get_value<wider_t>,
wider_int_for<element_t>,
and fixed_point<element_t>
internally, all of which require an integer element type). Scaling is
routed through the type’s own operator*
and operator/,
so wrappers can check for overflow and containers can scale
element-wise. The factor type is wider_int_for<element_t>
— a wider integer of matching sign
(e.g. int64_t
for int16_t,
uint64_t for
uint16_t) —
to prevent intermediate overflow in rational-magnitude conversions:
concept UsesIntegerScaling =
detail::integral<representation_underlying_type_t<T>> &&
requires(T value, wider_int_for<representation_underlying_type_t<T>> wf) {
{ value * wf };
{ value / wf };
};[ Note: detail::integral
is used rather than std::integral
because on GCC in strict mode (-std=c++20)
std::integral<__int128>
is false —
the standard traits are not specialized for
__int128
outside GNU extensions. When the platform lacks __SIZEOF_INT128__
entirely, int128_t and
uint128_t are software-emulation
types that also do not satisfy std::integral.
detail::integral
patches both gaps:
template<typename T>
concept detail::integral =
std::integral<T> ||
std::same_as<std::remove_cv_t<T>, int128_t> ||
std::same_as<std::remove_cv_t<T>, uint128_t>;The scaling engine internals
(get_value,
wider_int_for,
fixed_point) are all specialized for
int128_t /
uint128_t, ensuring the full integer
scaling pipeline works correctly for 128-bit element types on all
supported compilers. — end note ]
Most standard types satisfy
MagnitudeScalable automatically. See
Scaling operators for how to provide
operator*
and operator/
for custom types.
When two quantities of convertible units are combined or converted,
the library applies the unit magnitude
M to the representation value via
scale<To>(M, value).
The built-in decision tree is:
The magnitude-aware path (operator*(T, UnitMagnitude))
is checked first — before any of the built-in paths. If a representation
type provides this operator, it has full control over how scaling is
performed and what type is returned. The built-in paths are only used as
a fallback.
The integer path
(UsesIntegerScaling) never promotes
values to floating-point, even for the rational and irrational
sub-paths. This is intentional: the user explicitly chose an integer
representation type, opting out of floating-point arithmetic. The
platform may lack FP hardware (embedded systems, DSPs), rely on
software-emulated FP, or enforce a no-FP policy. The library respects
that choice throughout unit conversion.
The design preference order is exact integer > exact
rational > approximate irrational: integer multiplication
keeps lossless conversions exact (42 * m
→ 42000 * mm
without floating-point rounding); rational factors are applied as
numerator × value ÷ denominator entirely in integer arithmetic; and
irrational factors (π/180, √2, …) fall back to a long double
approximation rounded to the target integer type.
The rational path computes value * numerator / denominator
entirely in integer arithmetic using widened types to prevent
intermediate overflow — for example, converting feet to metres
multiplies by 3048 before dividing by 10000, which would overflow a
64-bit integer for values above ~3×10¹⁵ without extra width:
Source type
|
Widened to
|
|---|---|
Signed ≤ 32 bits
(int8_t…int32_t) |
int64_t |
| Unsigned ≤ 32 bits | uint64_t |
int64_t |
__int128
or equivalent signed 128-bit |
uint64_t |
unsigned __int128
or equivalent unsigned |
Using long double
instead would violate the no-FP principle and introduce rounding
(0.3048 is
not exactly representable in binary floating-point); on ARM / Apple
Silicon long double == double
anyway, giving no extra range.
Double-width arithmetic avoids most UB during intermediate scaling,
but cannot prevent overflow in the final result when that result doesn’t
fit in the target type. Runtime overflow detection requires a
representation type that checks arithmetic operations — a
checked-integer wrapper (e.g., safe_int<T>
from mp-units) satisfies
UsesIntegerScaling and will trap
overflows in the final result.
Aggregate representation types that store multiple
independently-scaled fields (e.g., uncertainty<T>
holding a central value and an error bound) implement operator*
and operator/
to distribute scaling across all internal fields; the built-in integer
and floating-point paths invoke those operators on the aggregate as a
unit.
For floating-point representation types
(UsesFloatingPointScaling), the unit
magnitude is reduced to a single
constexpr
scalar (a
double or
long double
value representing the exact mathematical ratio) and applied as a single
multiplication: value * factor.
This matches what equivalent hand-written code would do. The library
makes no stronger precision guarantee than the underlying floating-point
operations provide — in particular, it does not mandate any specific ULP
bound. This is consistent with the rest of the C++ standard library,
including std::chrono::duration,
which likewise leaves floating-point conversion precision to the quality
of the implementation.
The concern raised that lazy evaluation could produce results
differing from hand-written code by more than a few ULPs does not apply
here: the magnitude is always a single compile-time constant
representing the full unit ratio, never a chain of intermediate
multiplications. Overflow to
+inf would
therefore only occur for values that would also overflow the equivalent
hand-written multiplication, which is a property of the value, not the
library.
For integer types, widened arithmetic (as described above) prevents intermediate overflow. For floating-point types, implementations are encouraged to choose a representation of the conversion factor that minimises precision loss, but this is a quality-of-implementation concern, not a normative requirement.
A type satisfies
UsesMagnitudeAwareScaling (see How Scaling Works) by providing operator*(T, UnitMagnitude)
as a hidden friend. Unlike the built-in numeric paths, this operator
receives the full compile-time unit magnitude and may return a
different type — for example, a range-validated
representation can adjust its bounds during conversion: constraining
values to [-180, 180] in degrees should produce a type constrained to
[-π, π] when converted to radians, otherwise the bounds would be
meaningless in the target unit:
// Example custom type (not provided by the library)
template<std::treat_as_floating_point T, auto Min, auto Max, typename Policy>
class bounded_value : /* ... */ {
public:
template<std::UnitMagnitude M>
[[nodiscard]] friend constexpr auto operator*(const bounded_value& val, M m)
{
constexpr T new_lo = std::scale<T>(M{}, T{Min});
constexpr T new_hi = std::scale<T>(M{}, T{Max});
const T scaled = std::scale<T>(m, val.value());
if constexpr (new_lo <= new_hi)
return bounded_value<T, new_lo, new_hi, Policy>(scaled);
else
return bounded_value<T, new_hi, new_lo, Policy>(scaled);
}
};The scale function handles
precision optimization automatically — when the magnitude’s inverse is
integral (e.g. degree-to-radian with π/180), it divides by the inverse
instead of multiplying, avoiding FP rounding errors.
The library calls value * M{}
in scale()
before trying the built-in paths. Because the return type may differ
from the input, quantity::in(unit)
propagates the new representation type through
sudo_cast, and the resulting
quantity (or
quantity_point) automatically uses
the scaled-bounds representation.
TODO
TODO
TODO
This chapter traces a complete, minimal example from user-facing code down through the library’s layers, showing how each piece fits together. The goal is to build an intuition for the design before reading the detailed specifications that follow.
A smoot is a unit of length equal to the height of Oliver Smoot (five feet and seven inches), famously used to measure the Harvard Bridge in 1958. Adding it to the library takes exactly one line:
import std;
inline constexpr struct smoot : std::named_unit<"smoot", std::mag<67> * std::usc::inch> {} smoot;
int main()
{
constexpr std::quantity dist = 364.4 * smoot;
std::println("Harvard Bridge length = {::N[.1f]} ({::N[.1f]}, {::N[.2f]}) ± 1 εar",
dist, dist.in(std::usc::foot), dist.in(std::si::metre));
}Output:
Harvard Bridge length = 364.4 smoot (2034.6 ft, 620.14 m) ± 1 εarThree things happen here: a unit is defined, a quantity is created, and the quantity is converted. Each layer is examined below.
Every unit ultimately traces back to a base unit — a unit
associated directly with a quantity kind rather than being defined
relative to another unit. The named_unit<Symbol, kind_of<QS>>
form is used for these coherent base units:
// base unit — anchored to the length quantity kind
struct metre : named_unit<"m", kind_of<isq::length>> {} metre;From there, US customary units are defined as a chain of scaled
aliases using the named_unit<Symbol, Scale>
form, each expressed in terms of the one above:
struct yard : named_unit<"yd", mag_ratio<9144, 10000> * si::metre> {} yard; // 0.9144 m
struct foot : named_unit<"ft", mag_ratio<1, 3> * yard> {} foot; // 1/3 yd
struct inch : named_unit<"in", mag_ratio<1, 12> * foot> {} inch; // 1/12 ftBoth forms assign a name and symbol to the unit and automatically
propagate the quantity kind down the chain —
inch is a unit of length
with no extra annotation.
The scaling factors mag_ratio<N, D>
and mag<N>
are compile-time rational numbers stored as products of prime powers. No
floating-point arithmetic occurs in the type system. When the library
traverses the chain from inch to
metre, it multiplies the accumulated
factors:
1 inch = (1/12) × (1/3) × (9144/10000) m = 127/5000 m = 0.0254 m (exact)
The conversion factor from smoot
to metre is therefore:
1 smoot = 67 × (127/5000) m = 8509/5000 m (exact rational)
This ratio is available entirely at compile time and applied to the stored value only at the point of an explicit conversion.
named_unit<"smoot", mag<67> * usc::inch>The expression mag<67> * usc::inch
produces a scaled_unit<mag<67>, inch>.
Wrapping it in named_unit:
struct smoot : named_unit<"smoot", mag<67> * usc::inch> {} smoot;does three things simultaneously:
"smoot"
for use in text output._base_type_ of the
scaled unit, making smoot a unit of
length.get_canonical_unit(smoot)
can recover the exact magnitude relative to
metre without any additional
bookkeeping.quantity typeThe central type of the library is:
template<Reference auto R, RepresentationOf<get_quantity_spec(R)> Rep = double>
class quantity {
public:
Rep numerical_value_is_an_implementation_detail_; // the only runtime datum
static constexpr Reference auto reference = R;
static constexpr QuantitySpec auto quantity_spec = get_quantity_spec(R); // isq::length
static constexpr Dimension auto dimension = quantity_spec.dimension; // dim_length
static constexpr Unit auto unit = get_unit(R); // smoot
using rep = Rep;
};The unit, quantity specification, and dimension are part of the type — they consume no storage and carry zero runtime overhead. Only the numerical value is stored.
A bare unit such as smoot also
satisfies the Reference concept
(since the quantity specification can be recovered from it via
get_quantity_spec), so it can serve
directly as the reference template argument.
364.4 * smootThe expression 364.4 * smoot
invokes:
template<typename FwdRep, Reference R, ...>
requires(!OffsetUnit<get_unit(R{})>)
constexpr quantity<R{}, Rep> operator*(FwdRep&& lhs, R) { return quantity{std::forward<FwdRep>(lhs), R{}}; }The result is quantity<smoot{}, double>
— a type that encodes smoot and
double as
compile-time template arguments and stores only
364.4 at
runtime.
dist.in(si::metre).in(ToU)
checks at compile time that ToU is a
valid unit for the same quantity specification, then applies the
conversion:
template<UnitOf<quantity_spec> ToU>
requires ImplicitScaling<unit, ToU{}, rep>
constexpr QuantityOf<quantity_spec> auto in(ToU) const;Internally, the conversion ratio is computed entirely at compile time:
constexpr UnitMagnitude auto c_mag = get_canonical_unit(From::unit).mag / get_canonical_unit(To::unit).mag;The exact rational 8509/5000
is then applied to the stored value at runtime. The fraction is
converted to a long double
constant at compile time, so the actual runtime
operation for a
double
representation is a single multiplication:
364.4 × 1.7018L ≈ 620.14
Scaling behaviour for other representation types (integers, fixed-point, custom types) is discussed in [How Scaling Works?].
The return type is quantity<si::metre{}, double>
holding
620.14.
Attempting to convert to an incompatible unit — for example
si::second —
is a compile-time error because
si::second
does not satisfy UnitOf<isq::length>.
{::N[.1f]}The library provides a std::formatter
specialization for quantity. The
format-specification grammar (described fully in Text output) gives independent control over the
numerical part and the unit symbol. For dist.in(si::metre)
formatted with {::N[.2f]},
the N[.2f]
sub-spec is forwarded to the Rep
formatter (producing "620.14"),
after which the formatter appends the unit symbol "m" —
yielding "620.14 m".
That is the full path from user code to bits: one stored
double, with
the unit, quantity kind, dimension, and conversion factor living
entirely in the C++ type system.
This is intentionally a minimal example. Many library features — quantity convertibility, generic interfaces and concepts, the affine space, representation type constraints, mixed-unit arithmetic, value casts, and more — are not used here and therefore not described. They are covered in the chapters that follow.
This chapter demonstrates key safety features through minimal compile-time examples.
Let’s start with a really simple example presenting basic operations that every physical quantities and units library should provide:
import std;
using namespace std::si::unit_symbols;
// simple numeric operations
static_assert(10 * km / 2 == 5 * km);
// conversions to common units
static_assert(1 * h == 3600 * s);
static_assert(1 * km + 1 * m == 1001 * m);
// derived quantities
static_assert(1 * km / (1 * s) == 1000 * m / s);
static_assert(2 * km / h * (2 * h) == 4 * km);
static_assert(2 * km / (2 * km / h) == 1 * h);
static_assert(2 * m * (3 * m) == 6 * m2);
static_assert(10 * km / (5 * km) == 2);
static_assert(1000 / (1 * s) == 1 * kHz);Try it in the Compiler Explorer.
The next example serves as a showcase of various features available in the [mp-units] library.
import std;
constexpr std::QuantityOf<std::isq::speed> auto avg_speed(std::QuantityOf<std::isq::length> auto d,
std::QuantityOf<std::isq::time> auto t)
{
return d / t;
}
int main()
{
using namespace std::si::unit_symbols;
using namespace std::yard_pound::unit_symbols;
constexpr std::quantity v1 = 110 * km / h;
constexpr std::quantity v2 = 70 * mph;
constexpr std::quantity v3 = avg_speed(220. * std::isq::distance[km], 2 * h);
constexpr std::quantity v4 = avg_speed(std::isq::distance(140. * mi), 2 * h);
constexpr std::quantity v5 = v3.in(m / s);
constexpr std::quantity v6 = value_cast<m / s>(v4);
constexpr std::quantity v7 = value_cast<int>(v6);
std::cout << v1 << '\n'; // 110 km/h
std::cout << std::setw(10) << std::setfill('*') << v2 << '\n'; // ***70 mi/h
std::cout << std::format("{:*^10}\n", v3); // *110 km/h*
std::println("{:%N in %U of %D}", v4); // 70 in mi/h of LT⁻¹
std::println("{::N[.2f]}", v5); // 30.56 m/s
std::println("{::N[.2f]U[dn]}", v6); // 31.29 m⋅s⁻¹
std::println("{:%N}", v7); // 31
}Try it in the Compiler Explorer.
This example estimates the process of filling a storage tank with some contents. It presents:
std::chrono::duration.import std;
namespace {
using namespace std::si::unit_symbols;
// add a custom quantity type of kind isq::length
inline constexpr struct horizontal_length : std::quantity_spec<std::isq::length> {} horizontal_length;
// add a custom derived quantity type of kind isq::area
// with a constrained quantity equation
inline constexpr struct horizontal_area : std::quantity_spec<horizontal_length * std::isq::width> {} horizontal_area;
inline constexpr auto g = 1 * std::si::standard_gravity;
inline constexpr auto air_density = std::isq::mass_density(1.225 * kg / m3);
class StorageTank {
std::quantity<horizontal_area[m2]> base_;
std::quantity<std::isq::height[m]> height_;
std::quantity<std::isq::mass_density[kg / m3]> density_ = air_density;
public:
constexpr StorageTank(const std::quantity<horizontal_area[m2]>& base, const std::quantity<isq::height[m]>& height) :
base_(base), height_(height)
{
}
constexpr void set_contents_density(const std::quantity<std::isq::mass_density[kg / m3]>& density)
{
assert(density > air_density);
density_ = density;
}
[[nodiscard]] constexpr std::QuantityOf<isq::weight> auto filled_weight() const
{
std::quantity volume = std::isq::volume(base_ * height_);
const std::QuantityOf<std::isq::mass> auto mass = density_ * volume;
return std::isq::weight(mass * g);
}
[[nodiscard]] constexpr std::quantity<std::isq::height[m]> fill_level(const std::quantity<std::isq::mass[kg]>& measured_mass) const
{
return height_ * measured_mass * g / filled_weight();
}
[[nodiscard]] constexpr std::quantity<std::isq::volume[m3]> spare_capacity(const std::quantity<std::isq::mass[kg]>& measured_mass) const
{
return (height_ - fill_level(measured_mass)) * base_;
}
};
class CylindricalStorageTank : public StorageTank {
public:
constexpr CylindricalStorageTank(const std::quantity<std::isq::radius[m]>& radius, const std::quantity<std::isq::height[m]>& height) :
StorageTank(std::quantity_cast<horizontal_area>(std::numbers::pi * pow<2>(radius)), height)
{
}
};
class RectangularStorageTank : public StorageTank {
public:
constexpr RectangularStorageTank(const std::quantity<horizontal_length[m]>& length, const std::quantity<isq::width[m]>& width,
const std::quantity<isq::height[m]>& height) :
StorageTank(length * width, height)
{
}
};
} // namespace
int main()
{
const std::quantity height = std::isq::height(200 * mm);
auto tank = RectangularStorageTank(horizontal_length(1'000 * mm), std::isq::width(500 * mm), height);
tank.set_contents_density(1'000 * kg / m3);
const auto duration = std::chrono::seconds{200};
const std::quantity fill_time = std::value_cast<int>(std::quantity{duration}); // time since starting fill
const std::quantity measured_mass = 20. * kg; // measured mass at fill_time
const std::quantity fill_level = tank.fill_level(measured_mass);
const std::quantity spare_capacity = tank.spare_capacity(measured_mass);
const std::quantity filled_weight = tank.filled_weight();
const std::QuantityOf<std::isq::mass_change_rate> auto input_flow_rate = measured_mass / fill_time;
const std::QuantityOf<std::isq::speed> auto float_rise_rate = fill_level / fill_time;
const std::QuantityOf<std::isq::time> auto fill_time_left = (height / fill_level - 1 * one) * fill_time;
const std::quantity fill_ratio = fill_level / height;
std::println("fill height at {} = {} ({} full)", fill_time, fill_level, fill_ratio.in(percent));
std::println("fill weight at {} = {} ({})", fill_time, filled_weight, filled_weight.in(N));
std::println("spare capacity at {} = {}", fill_time, spare_capacity);
std::println("input flow rate = {}", input_flow_rate);
std::println("float rise rate = {}", float_rise_rate);
std::println("tank full E.T.A. at current flow rate = {}", fill_time_left.in(s));
}The above code outputs:
fill height at 200 s = 0.04 m (20% full)
fill weight at 200 s = 100 g₀ kg (980.665 N)
spare capacity at 200 s = 0.08 m³
input flow rate = 0.1 kg/s
float rise rate = 2e-04 m/s
tank full E.T.A. at current flow rate = 800 s
Try it in the Compiler Explorer.
The following example codifies the history of a famous issue during the construction of a bridge across the Rhine River between the German and Swiss parts of the town Laufenburg [Hochrheinbrücke]. It also nicely presents how the Affine Space is being modeled in the library.
import std;
using namespace std::si::unit_symbols;
inline constexpr struct amsterdam_sea_level : std::absolute_point_origin<isq::altitude> {
} amsterdam_sea_level;
inline constexpr struct mediterranean_sea_level : std::relative_point_origin<amsterdam_sea_level - 27 * cm> {
} mediterranean_sea_level;
using altitude_DE = std::quantity_point<std::isq::altitude[m], amsterdam_sea_level>;
using altitude_CH = std::quantity_point<std::isq::altitude[m], mediterranean_sea_level>;
template<auto R, typename Rep>
std::ostream& operator<<(std::ostream& os, std::quantity_point<R, altitude_DE::point_origin, Rep> alt)
{
return os << alt.quantity_ref_from(altitude_DE::point_origin) << " AMSL(DE)";
}
template<auto R, typename Rep>
std::ostream& operator<<(std::ostream& os, std::quantity_point<R, altitude_CH::point_origin, Rep> alt)
{
return os << alt.quantity_ref_from(altitude_CH::point_origin) << " AMSL(CH)";
}
template<auto R, typename Rep>
struct std::formatter<std::quantity_point<R, altitude_DE::point_origin, Rep>> : std::formatter<std::quantity<R, Rep>> {
template<typename FormatContext>
auto format(const std::quantity_point<R, altitude_DE::point_origin, Rep>& alt, FormatContext& ctx) const
{
std::formatter<std::quantity<R, Rep>>::format(alt.quantity_ref_from(altitude_DE::point_origin), ctx);
return std::format_to(ctx.out(), " AMSL(DE)");
}
};
template<auto R, typename Rep>
struct std::formatter<std::quantity_point<R, altitude_CH::point_origin, Rep>> : std::formatter<std::quantity<R, Rep>> {
template<typename FormatContext>
auto format(const std::quantity_point<R, altitude_CH::point_origin, Rep>& alt, FormatContext& ctx) const
{
std::formatter<std::quantity<R, Rep>>::format(alt.quantity_ref_from(altitude_CH::point_origin), ctx);
return std::format_to(ctx.out(), " AMSL(CH)");
}
};
int main()
{
// expected bridge altitude in a specific reference system
std::quantity_point expected_bridge_alt = amsterdam_sea_level + 330 * m;
// some nearest landmark altitudes on both sides of the river
// equal but not equal ;-)
altitude_DE landmark_alt_DE = altitude_DE::point_origin + 300 * m;
altitude_CH landmark_alt_CH = altitude_CH::point_origin + 300 * m;
// artifical deltas from landmarks of the bridge base on both sides of the river
std::quantity delta_DE = std::isq::height(3 * m);
std::quantity delta_CH = std::isq::height(-2 * m);
// artificial altitude of the bridge base on both sides of the river
std::quantity_point bridge_base_alt_DE = landmark_alt_DE + delta_DE;
std::quantity_point bridge_base_alt_CH = landmark_alt_CH + delta_CH;
// artificial height of the required bridge pilar height on both sides of the river
std::quantity bridge_pilar_height_DE = expected_bridge_alt - bridge_base_alt_DE;
std::quantity bridge_pilar_height_CH = expected_bridge_alt - bridge_base_alt_CH;
std::println("Bridge pillars height:");
std::println("- Germany: {}", bridge_pilar_height_DE);
std::println("- Switzerland: {}", bridge_pilar_height_CH);
// artificial bridge altitude on both sides of the river in both systems
std::quantity_point bridge_road_alt_DE = bridge_base_alt_DE + bridge_pilar_height_DE;
std::quantity_point bridge_road_alt_CH = bridge_base_alt_CH + bridge_pilar_height_CH;
std::println("Bridge road altitude:");
std::println("- Germany: {}", bridge_road_alt_DE);
std::println("- Switzerland: {}", bridge_road_alt_CH);
std::println("Bridge road altitude relative to the Amsterdam Sea Level:");
std::println("- Germany: {}", bridge_road_alt_DE.quantity_from(amsterdam_sea_level));
std::println("- Switzerland: {}", bridge_road_alt_CH.quantity_from(amsterdam_sea_level));
}The above provides the following text output:
Bridge pillars height:
- Germany: 27 m
- Switzerland: 3227 cm
Bridge road altitude:
- Germany: 330 m AMSL(DE)
- Switzerland: 33027 cm AMSL(CH)
Bridge road altitude relative to the Amsterdam Sea Level:
- Germany: 330 m
- Switzerland: 33000 cm
Try it in the Compiler Explorer.
Every measurement can (and probably should) be modelled as a
quantity_point and this is a perfect
example of such a use case.
This example implements a simplified scenario of measuring voltage read from hardware through a mapped 16-bits register. The actual voltage range of [-10 V, 10 V] is mapped to [0, 65534] on hardware and the value 65535 is used for error reporting. Translation of the value requires not only scaling of the value but also applying of an offset.
import std;
// real voltage range
inline constexpr int min_voltage = -10;
inline constexpr int max_voltage = 10;
inline constexpr int voltage_range = max_voltage - min_voltage;
// hardware encoding of voltage
using voltage_hw_t = std::uint16_t;
inline constexpr voltage_hw_t voltage_hw_error = std::numeric_limits<voltage_hw_t>::max();
inline constexpr voltage_hw_t voltage_hw_min = 0;
inline constexpr voltage_hw_t voltage_hw_max = voltage_hw_error - 1;
inline constexpr voltage_hw_t voltage_hw_range = voltage_hw_max - voltage_hw_min;
inline constexpr voltage_hw_t voltage_hw_zero = voltage_hw_range / 2;
inline constexpr struct hw_voltage_origin :
std::relative_point_origin<std::point<std::si::volt>(min_voltage)> {} hw_voltage_origin;
inline constexpr struct hw_voltage_unit :
std::named_unit<"hwV", std::mag_ratio<voltage_range, voltage_hw_range> * std::si::volt, hw_voltage_origin> {} hw_voltage_unit;
using hw_voltage_quantity_point = std::quantity_point<hw_voltage_unit, hw_voltage_origin, voltage_hw_t>;
// mapped HW register
volatile voltage_hw_t hw_voltage_value;
std::optional<hw_voltage_quantity_point> read_hw_voltage()
{
voltage_hw_t local_copy = hw_voltage_value;
if (local_copy == voltage_hw_error) return std::nullopt;
return std::point<hw_voltage_unit>(local_copy);
}
void print(std::QuantityPoint auto qp)
{
std::println("{:10} ({:5})", qp.quantity_from_unit_zero(),
std::value_cast<double, si::volt>(qp).quantity_from_unit_zero());
}
int main()
{
// simulate reading of 3 values from the hardware
hw_voltage_value = voltage_hw_min;
std::quantity_point qp1 = read_hw_voltage().value();
hw_voltage_value = voltage_hw_zero;
std::quantity_point qp2 = read_hw_voltage().value();
hw_voltage_value = voltage_hw_max;
std::quantity_point qp3 = read_hw_voltage().value();
print(qp1);
print(qp2);
print(qp3);
}The above prints:
0 hwV (-10 V)
32767 hwV ( 0 V)
65534 hwV ( 10 V)
Try it in the Compiler Explorer.
Users can easily define new quantities and units for domain-specific use-cases. This example from digital signal processing domain will show how to define custom strongly typed dimensionless quantities, units for them, and how they can be converted to time measured in milliseconds:
import std;
namespace ni {
// quantities
inline constexpr struct SampleCount : std::quantity_spec<std::dimensionless, std::is_kind> {} SampleCount;
inline constexpr struct SampleDuration : std::quantity_spec<std::isq::period_duration> {} SampleDuration;
inline constexpr struct SamplingRate : std::quantity_spec<std::isq::frequency, SampleCount / SampleDuration> {} SamplingRate;
inline constexpr struct UnitSampleAmount : std::quantity_spec<std::dimensionless, std::is_kind> {} UnitSampleAmount;
inline constexpr auto Amplitude = UnitSampleAmount;
inline constexpr auto Level = UnitSampleAmount;
inline constexpr struct Power : std::quantity_spec<Level * Level> {} Power;
inline constexpr struct MIDIClock : std::quantity_spec<std::dimensionless, std::is_kind> {} MIDIClock;
inline constexpr struct BeatCount : std::quantity_spec<std::dimensionless, std::is_kind> {} BeatCount;
inline constexpr struct BeatDuration : std::quantity_spec<std::isq::period_duration> {} BeatDuration;
inline constexpr struct Tempo : std::quantity_spec<std::isq::frequency, BeatCount / BeatDuration> {} Tempo;
// units
inline constexpr struct Sample : std::named_unit<"Smpl", std::one, std::kind_of<SampleCount>> {} Sample;
inline constexpr struct SampleValue : std::named_unit<"PCM", std::one, std::kind_of<UnitSampleAmount>> {} SampleValue;
inline constexpr struct MIDIPulse : std::named_unit<"p", std::one, std::kind_of<MIDIClock>> {} MIDIPulse;
inline constexpr struct QuarterNote : std::named_unit<"q", std::one, std::kind_of<BeatCount>> {} QuarterNote;
inline constexpr struct HalfNote : std::named_unit<"h", std::mag<2> * QuarterNote> {} HalfNote;
inline constexpr struct DottedHalfNote : std::named_unit<"h.", std::mag<3> * QuarterNote> {} DottedHalfNote;
inline constexpr struct WholeNote : std::named_unit<"w", std::mag<4> * QuarterNote> {} WholeNote;
inline constexpr struct EighthNote : std::named_unit<"8th", std::mag_ratio<1, 2> * QuarterNote> {} EighthNote;
inline constexpr struct DottedQuarterNote : std::named_unit<"q.", std::mag<3> * EighthNote> {} DottedQuarterNote;
inline constexpr struct QuarterNoteTriplet : std::named_unit<"qt", std::mag_ratio<1, 3> * HalfNote> {} QuarterNoteTriplet;
inline constexpr struct SixteenthNote : std::named_unit<"16th", std::mag_ratio<1, 2> * EighthNote> {} SixteenthNote;
inline constexpr struct DottedEighthNote : std::named_unit<"q.", std::mag<3> * SixteenthNote> {} DottedEighthNote;
inline constexpr auto Beat = QuarterNote;
inline constexpr struct BeatsPerMinute : std::named_unit<"bpm", Beat / std::si::minute> {} BeatsPerMinute;
inline constexpr struct MIDIPulsePerQuarter : std::named_unit<"ppqn", MIDIPulse / QuarterNote> {} MIDIPulsePerQuarter;
namespace unit_symbols {
inline constexpr auto Smpl = Sample;
inline constexpr auto pcm = SampleValue;
inline constexpr auto p = MIDIPulse;
inline constexpr auto n_wd = 3 * HalfNote;
inline constexpr auto n_w = WholeNote;
inline constexpr auto n_hd = DottedHalfNote;
inline constexpr auto n_h = HalfNote;
inline constexpr auto n_qd = DottedQuarterNote;
inline constexpr auto n_q = QuarterNote;
inline constexpr auto n_qt = QuarterNoteTriplet;
inline constexpr auto n_8thd = DottedEighthNote;
inline constexpr auto n_8th = EighthNote;
inline constexpr auto n_16th = SixteenthNote;
}
std::quantity<BeatsPerMinute, float> GetTempo()
{
return 110 * BeatsPerMinute;
}
std::quantity<MIDIPulsePerQuarter, unsigned> GetPPQN()
{
return 960 * MIDIPulse / QuarterNote;
}
std::quantity<MIDIPulse, unsigned> GetTransportPos()
{
return 15'836 * MIDIPulse;
}
std::quantity<SamplingRate[std::si::hertz], float> GetSampleRate()
{
return 44'100.f * std::si::hertz;
}
}
int main()
{
using namespace ni::unit_symbols;
using namespace std::si::unit_symbols;
const std::quantity sr1 = ni::GetSampleRate();
const std::quantity sr2 = 48'000.f * Smpl / s;
const std::quantity samples = 512 * Smpl;
const std::quantity sampleTime1 = (samples.in<float>() / sr1).in(s);
const std::quantity sampleTime2 = (samples.in<float>() / sr2).in(ms);
const std::quantity sampleDuration1 = std::inverse<ms>(sr1);
const std::quantity sampleDuration2 = std::inverse<ms>(sr2);
const std::quantity rampTime = 35.f * ms;
const std::quantity rampSamples1 = ni::SampleCount((rampTime * sr1).in(one)).force_in<int>(Smpl);
const std::quantity rampSamples2 = (rampTime * sr2).force_in<int>(Smpl);
std::println("Sample rate 1 is: {}", sr1);
std::println("Sample rate 2 is: {}", sr2);
std::println("{} @ {} is {::N[.5f]}", samples, sr1, sampleTime1);
std::println("{} @ {} is {::N[.5f]}", samples, sr2, sampleTime2);
std::println("One sample @ {} is {::N[.5f]}", sr1, sampleDuration1);
std::println("One sample @ {} is {::N[.5f]}", sr2, sampleDuration2);
std::println("{} is {} @ {}", rampTime, rampSamples1, sr1);
std::println("{} is {} @ {}", rampTime, rampSamples2, sr2);
const std::quantity sampleValue = -0.4f * pcm;
const std::quantity power1 = sampleValue * sampleValue;
const std::quantity power2 = -0.2 * pow<2>(pcm);
const std::quantity tempo = ni::GetTempo();
const std::quantity reverbBeats = 1 * n_qd;
const std::quantity reverbTime = reverbBeats / tempo;
const std::quantity pulsePerQuarter = std::value_cast<float>(ni::GetPPQN());
const std::quantity transportPosition = ni::GetTransportPos();
const std::quantity transportBeats = (transportPosition / pulsePerQuarter).in(n_q);
const std::quantity transportTime = (transportBeats / tempo).in(s);
std::println("SampleValue is: {}", sampleValue);
std::println("Power 1 is: {}", power1);
std::println("Power 2 is: {}", power2);
std::println("Tempo is: {}", tempo);
std::println("Reverb Beats is: {}", reverbBeats);
std::println("Reverb Time is: {}", reverbTime.in(s));
std::println("Pulse Per Quarter is: {}", pulsePerQuarter);
std::println("Transport Position is: {}", transportPosition);
std::println("Transport Beats is: {}", transportBeats);
std::println("Transport Time is: {}", transportTime);
// auto error = 1 * Smpl + 1 * pcm + 1 * p + 1 * Beat; // Compile-time error
}The above code outputs:
Sample rate 1 is: 44100 Hz
Sample rate 2 is: 48000 Smpl/s
512 Smpl @ 44100 Hz is 0.01161 s
512 Smpl @ 48000 Smpl/s is 10.66667 ms
One sample @ 44100 Hz is 0.02268 ms
One sample @ 48000 Smpl/s is 0.02083 ms
35 ms is 1543 Smpl @ 44100 Hz
35 ms is 1680 Smpl @ 48000 Smpl/s
SampleValue is: -0.4 PCM
Power 1 is: 0.16000001 PCM²
Power 2 is: -0.2 PCM²
Tempo is: 110 bpm
Reverb Beats is: 1 q.
Reverb Time is: 0.8181818 s
Pulse Per Quarter is: 960 ppqn
Transport Position is: 15836 p
Transport Beats is: 16.495832 q
Transport Time is: 8.997726 s
Try it in the Compiler Explorer.
Note: More about this example can be found in “Exploration of Strongly-typed Units in C++: A Case Study from Digital Audio” CppCon 2023 talk by Roth Michaels.
Units-only is not a good design for a quantities and units library. It works to some extent, but plenty of use cases can’t be addressed, and for those that somehow work, we miss important safety improvements provided by additional abstractions in this chapter. But before we talk about those extensions, let’s first discuss some limitations of the units-only solution.
Note: The issues described below do not apply to the proposed library, because with the proposed interfaces, even if we decide to only use units, they are still backed up by quantity kinds under the framework’s hood.
A common requirement in the domain is to write unit-agnostic generic
interfaces. For example, let’s try to implement a generic
avg_speed function template that
takes a quantity of any unit and produces the result. So if we call it
with distance in km and
time in h, we will get
km / h as a
result, but if we call it with mi
and h, we expect
mi / h to be
returned.
template<Unit auto U1, typename Rep1, Unit auto U2, typename Rep2>
auto avg_speed(quantity<U1, Rep1> distance, quantity<U2, Rep2> time)
{
return distance / time;
}
quantity speed = avg_speed(120 * km, 2 * h);This function works but does not provide any type safety to the users. The function arguments can be easily reordered on the call site. Also, we do not get any information about the return type of the function and any safety to ensure that the function logic actually returns a quantity of speed.
To improve safety, with a units-only library, we have to write the function in the following way:
template<typename Rep1, typename Rep2>
quantity<si::metre / si::second, decltype(Rep1{} / Rep2{})> avg_speed(quantity<si::metre, Rep1> distance,
quantity<si::second, Rep2> time)
{
return distance / time;
}
avg_speed(120 * km, 2 * h).in(km / h);Despite being safer, the above code decreased the performance because we always pay for the conversion at the function’s input and output.
Moreover, in a good library, the above code should not compile. The
reason for this is that even though the conversion from
km to
m and from
h to
s is considered value-preserving, it
is not true in the opposite direction. When we will try to convert the
result stored in an integral type from the unit of
m/s to
km/h we will
inevitably loose some data.
We could try to provide concepts like ScaledUnitOf<si::metre>
that would take a set of units while trying to constrain them somehow,
but it leads to even more problems with the unit definitions. For
example, are Hz and
Bq just scaled versions of 1/s?
If we constrain the interface to just prefixed units, then litre and a
cubic metre or kilometre and mile will be incompatible. What about
radian and steradian or a litre per 100 kilometre (popular unit of a
fuel consumption) and a squared metre? Should those be compatible?
Sometimes, we need to define several units describing the same quantity but which should not convert to each other in the library’s framework. A typical example here is currency. A user may want to define EURO and USD as units of currency, so both of them can be used for such quantities. However, it is impossible to predefine one fixed conversion factor for those, as a currency exchange rate varies over time, and the library’s framework can’t provide such an information as an input to the built-in conversion function. User’s application may have more information in this domain and handle such a conversion at runtime with custom logic (e.g., using an additional time point function argument). If we would like to model that in a unit-only solution, how can we specify that EURO and USD are units of quantities of currency, but are not convertible to each other?
To prevent the above issues, most of the libraries on the market introduce dimension abstraction. Thanks to that, we could solve the first issue of the previous chapter with:
QuantityOf<dim_speed> auto avg_speed(QuantityOf<dim_length> auto distance,
QuantityOf<dim_time> auto time)
{
return distance / time;
}and the second one by specifying that both EURO and USD are units of
dim_currency. This is a significant
improvement but still has some issues.
Let’s first look again at the above solution. A domain expert seeing this code will immediately say there is no such thing as a speed dimension. The ISQ specifies only 7 dimensions with unique symbols assigned, and the dimensions of all the ISQ quantities are created as a vector product of those. For example, a quantity of speed has a dimension of \(L^1T^{-1}\). So, to be physically correct, the above code should be rewritten as:
QuantityOf<dim_length / dim_time> auto avg_speed(QuantityOf<dim_length> auto distance,
QuantityOf<dim_time> auto time)
{
return distance / time;
}Most of the libraries on the market ignore this fact and try to model distinct quantities through their dimensions, giving a false sense of safety. A dimension is not enough to describe a quantity. This has been known for a long time now. The [Measurement Data] report from 1996 says explicitly, “Dimensional analysis does not adequately model the semantics of measurement data”.
In the following chapters, we will see a few use cases that can’t be solved with an approach that only relies on units or dimensions.
The [SI] provides several units for distinct quantities of the same dimension but different kinds. For example:
There are many more similar examples in [ISO/IEC 80000]. For example, storage capacity quantity can be measured in units of one, bit, octet, and byte.
The above conflicts can’t be solved with dimensions, and they yield many safety issues. For example, we can ask ourselves what should be the result of the following:
quantity q = 1 * Hz + 1 * Bq;quantity<Gy> q = 42 * Sv;bool b = (1 * rad + 1 * bit) == 2 * sr;None of the above code should compile, but most of the libraries on
the market happily accept it and provide meaningless results. Some of
them decide not to define one or more of the above units at all to avoid
potential safety issues. For example, the [Au] library does not define
Sv to avoid mixing it up with
Gy.
Even if some quantities do not have a specially assigned unit, they may still have a totally different physical meaning even if they share the same dimension:
Again, we don’t want to accidentally mix those.
Even if we somehow address all the above, there are plenty of use cases that still can’t be safely implemented with such abstractions.
Let’s consider that we want to implement a freight transport application to position cargo in the container. In majority of the products on the market we will end up with something like:
class Box {
length length_;
length width_;
length height_;
public:
Box(length l, length w, length h): length_(l), width_(w), height_(h) {}
area floor() const { return length_ * width_; }
// ...
};Box my_box(2 * m, 3 * m, 1 * m);Such interfaces are not much safer than just using plain fundamental
types (e.g.,
double). One
of the main reasons of using a quantities and units library was to
introduce strong-type interfaces to prevent such issues. In this
scenario, we need to be able to discriminate between length,
width, and height of the package.
A similar but also really important use case is in aviation. The current altitude is a totally different quantity than the distance to the destination. The same is true for forward speed and sink rate. We do not want to accidentally mix those.
When we deal with energy, we should be able to implicitly construct it from a proper product of any mass, length, and time. However, when we want to calculate gravitational potential energy, we may not want it to be implicitly initialized from any expression of matching dimensions. Such an implicit construction should be allowed only if we multiply a mass with acceleration of free fall and height. All other conversions should have an explicit annotation to make it clear that something potentially unsafe is being done in the code. Also, we should not be able to assign a potential energy to a quantity of kinetic energy. However, both of them (possibly accumulated with each other) should be convertible to a mechanical energy quantity.
mass m = 1 * kg;
length l = 1 * m;
time t = 1 * s;
acceleration_of_free_fall g = 9.81 * m / s2;
height h = 1 * m;
speed v = 1 * m / s;
energy e = m * pow<2>(l) / pow<2>(t); // OK
potential_energy ep1 = e; // should not compile
potential_energy ep2 = static_cast<potential_energy>(e); // OK
potential_energy ep3 = m * g * h; // OK
kinetic_energy ek1 = m * pow<2>(v) / 2; // OK
kinetic_energy ek2 = ep3 + ek1; // should not compile
mechanical_energy me = ep3 + ek1; // OKYet another example comes from the audio industry. In the audio
software, we want to treat specific counts (e.g., beats,
samples) as separate quantities. We could assign dedicated base
dimensions to them. However, if we divide them by duration, we
should obtain a quantity convertible to frequency and even be
able to express the result in a unit of
Hz. With the dedicated dimensions
approach, this wouldn’t work as the dimension of frequency is just \(T^{-1}\), which would not match the results
of our dimensional equations. This is why we can’t assign dedicated
dimensions to such counts.
The last example that we want to mention here comes from finance. This time, we need to model currency volume as a special quantity of currency. currency volume can be obtained by multiplying currency by the dimensionless market quantity. Of course, both currency and currency volume should be expressed in the same units (e.g., USD).
None of the above scenarios can be addressed with just units and dimensions. We need a better abstraction to safely implement them.
A system of quantities is a set of quantities together with a set of noncontradictory equations relating those quantities.
The International System of Quantities (ISQ) is a system of quantities based on the seven base quantities: length, mass, time, electric current, thermodynamic temperature, amount of substance, and luminous intensity. This system of quantities is published in [ISO/IEC 80000], “Quantities and units”.
A system of units is a set of base units and derived units, together with their multiples and submultiples, defined in accordance with given rules, for a given system of quantities.
The International System of Units (SI) is a system of units, based on the International System of Quantities, their names and symbols, including a series of prefixes and their names and symbols, together with rules for their use, adopted by the General Conference on Weights and Measures (CGPM).
The physical units libraries on the market typically only focus on modeling one or more systems of units. However, this is not the only system kind to model. Another, and maybe even more important, is a system of quantities. The most important example here is the International System of Quantities (ISQ) defined by [ISO/IEC 80000].
As it was described in Limitations of dimensions, dimension is not enough to describe a quantity. We need a better abstraction to provide safety to our calculations.
The [ISO/IEC Guide 99] says:
[ISO/IEC 80000] also explicitly notes:
Measurement units of quantities of the same quantity dimension may be designated by the same name and symbol even when the quantities are not of the same kind. For example, joule per kelvin and J/K are respectively the name and symbol of both a measurement unit of heat capacity and a measurement unit of entropy, which are generally not considered to be quantities of the same kind. However, in some cases special measurement unit names are restricted to be used with quantities of specific kind only. For example, the measurement unit ‘second to the power minus one’ (1/s) is called hertz (Hz) when used for frequencies and becquerel (Bq) when used for activities of radionuclides. As another example, the joule (J) is used as a unit of energy, but never as a unit of moment of force, i.e. the newton metre (N · m).
Those provide answers to all the issues mentioned above. More than one quantity may be defined for the same dimension:
Two quantities can’t be added, subtracted, or compared unless they belong to the same quantity kind.
[ISO/IEC 80000] specifies hundreds of different quantities. Plenty of various kinds are provided, and often, each kind contains more than one quantity. It turns out that such quantities form a hierarchy of quantities of the same kind.
For example, here are all quantities of the kind length provided in [ISO/IEC 80000] (part 1):
Each of the above quantities expresses some kind of length, and each can be measured with meters, which is the unit defined by the [SI] for quantities of length. However, each has different properties, usage, and sometimes even a different character (position vector and displacement are vector quantities).
The below presents how such a hierarchy tree can be defined in the library:
inline constexpr struct dim_length : base_dimension<"L"> {} dim_length;
inline constexpr struct length : quantity_spec<dim_length> {} length;
inline constexpr struct width : quantity_spec<length> {} width;
inline constexpr auto breadth = width;
inline constexpr struct height : quantity_spec<length> {} height;
inline constexpr auto depth = height;
inline constexpr auto altitude = height;
inline constexpr struct thickness : quantity_spec<width> {} thickness;
inline constexpr struct diameter : quantity_spec<width> {} diameter;
inline constexpr struct radius : quantity_spec<width> {} radius;
inline constexpr struct radius_of_curvature : quantity_spec<radius> {} radius_of_curvature;
inline constexpr struct path_length : quantity_spec<length> {} path_length;
inline constexpr auto arc_length = path_length;
inline constexpr struct distance : quantity_spec<path_length> {} distance;
inline constexpr struct radial_distance : quantity_spec<distance> {} radial_distance;
inline constexpr struct wavelength : quantity_spec<length> {} wavelength;
inline constexpr struct displacement : quantity_spec<length, quantity_character::vector> {} displacement;
inline constexpr struct position_vector : quantity_spec<displacement> {} position_vector;In the above code:
length takes the base dimension
to indicate that we are creating a base quantity that will serve as a
root for a tree of quantities of the same kind,width and following quantities
are branches and leaves of this tree with the parent always provided as
the first argument to quantity_spec
class template,breadth is an alias name for the
same quantity as width.Please note that some quantities may be specified by [ISO/IEC 80000] as vector or tensor
quantities (e.g., displacement).
Quantity conversion rules can be defined based on the same hierarchy of quantities of kind length.
Implicit conversions
static_assert(implicitly_convertible(isq::width, isq::length));
static_assert(implicitly_convertible(isq::radius, isq::length));
static_assert(implicitly_convertible(isq::radius, isq::width));Implicit conversions are allowed on copy-initialization:
void foo(quantity<isq::length[m]> q);quantity<isq::width[m]> q1 = 42 * m;
quantity<isq::length[m]> q2 = q1; // implicit quantity conversion
foo(q1); // implicit quantity conversionExplicit conversions
static_assert(!implicitly_convertible(isq::length, isq::width));
static_assert(!implicitly_convertible(isq::length, isq::radius));
static_assert(!implicitly_convertible(isq::width, isq::radius));
static_assert(explicitly_convertible(isq::length, isq::width));
static_assert(explicitly_convertible(isq::length, isq::radius));
static_assert(explicitly_convertible(isq::width, isq::radius));Explicit conversions are forced by passing the quantity to a call
operator of a quantity_spec type or
by calling quantity’s explicit
constructor::
void foo(quantity<isq::height[m]> q);quantity<isq::length[m]> q1 = 42 * m;
quantity<isq::height[m]> q2 = isq::height(q1); // explicit quantity conversion
quantity<isq::height[m]> q3(q1); // direct initialization
foo(isq::height(q1)); // explicit quantity conversionExplicit casts
static_assert(!implicitly_convertible(isq::height, isq::width));
static_assert(!explicitly_convertible(isq::height, isq::width));
static_assert(castable(isq::height, isq::width));Explicit casts are forced with a dedicated
quantity_cast function:
void foo(quantity<isq::height[m]> q);quantity<isq::width[m]> q1 = 42 * m;
quantity<isq::height[m]> q2 = quantity_cast<isq::height>(q1); // explicit quantity cast
foo(quantity_cast<isq::height>(q1)); // explicit quantity castNo conversion
static_assert(!implicitly_convertible(isq::time, isq::length));
static_assert(!explicitly_convertible(isq::time, isq::length));
static_assert(!castable(isq::time, isq::length));Even the explicit casts will not force such a conversion:
void foo(quantity<isq::length[m]>);quantity<isq::length[m]> q1 = 42 * s; // Compile-time error
foo(quantity_cast<isq::length>(42 * s)); // Compile-time error[ISO/IEC Guide 99] explicitly states that width and height are quantities of the same kind and as such they:
If we take the above for granted, the only reasonable result of 1 * width + 1 * height
is 2 * length,
where the result of length is known
as a common quantity type. A result of such an equation is always the
first common node in a hierarchy tree of the same kind. For example:
static_assert((isq::width(1 * m) + isq::height(1 * m)).quantity_spec == isq::length);
static_assert((isq::thickness(1 * m) + isq::radius(1 * m)).quantity_spec == isq::width);
static_assert((isq::distance(1 * m) + isq::path_length(1 * m)).quantity_spec == isq::path_length);One could argue that allowing to add or compare quantities of height and width might be a safety issue, but we need to be consistent with the requirements of [ISO/IEC 80000]. Moreover, from our experience, disallowing such operations and requiring an explicit cast to a common quantity in every single place makes the code so cluttered with casts that it nearly renders the library unusable.
Fortunately, the above-mentioned conversion rules make the code safe by construction anyway. Let’s analyze the following example:
inline constexpr struct horizontal_length : quantity_spec<isq::length> {} horizontal_length;
namespace christmas {
struct gift {
quantity<horizontal_length[m]> length;
quantity<isq::width[m]> width;
quantity<isq::height[m]> height;
};
std::array<quantity<isq::length[m]>, 2> gift_wrapping_paper_size(const gift& g)
{
quantity dim1 = 2 * g.width + 2 * g.height + 0.5 * g.width;
quantity dim2 = g.length + 2 * 0.75 * g.height;
return { dim1, dim2 };
}
} // namespace christmas
int main()
{
const christmas::gift lego = { horizontal_length(40 * cm), isq::width(30 * cm), isq::height(15 * cm) };
auto paper = christmas::gift_wrapping_paper_size(lego);
std::cout << "Paper needed to pack a lego box:\n";
std::cout << "- " << paper[0] << " X " << paper[1] << "\n"; // - 1.05 m X 0.625 m
std::cout << "- area = " << paper[0] * paper[1] << "\n"; // - area = 0.65625 m²
}In the beginning, we introduce a custom quantity
horizontal_length of a kind
length, which then, together with
isq::width
and
isq::height,
are used to define the dimensions of a Christmas gift. Next, we provide
a function that calculates the dimensions of a gift wrapping paper with
some wraparound. The result of both those expressions is a quantity of
isq::length,
as this is the closest common quantity for the arguments used in this
quantity equation.
Regarding safety, it is important to mention here, that thanks to the conversion rules provided above, it would be impossible to accidentally do the following:
void foo(quantity<horizontal_length[m]> q);
quantity<isq::width[m]> q1 = dim1; // Compile-time error
quantity<isq::height[m]> q2{dim1}; // Compile-time error
foo(dim1); // Compile-time errorThe reason of compilation errors above is the fact that
isq::length
is not implicitly convertible to the quantities defined based on it. To
make the above code compile, an explicit conversion of a quantity type
is needed:
void foo(quantity<horizontal_length[m]> q);
quantity<isq::width[m]> q1 = isq::width(dim1);
quantity<isq::height[m]> q2{isq::height(dim1)};
foo(horizontal_length(dim1));To summarize, rules for addition, subtraction, and comparison of quantities improve the library usability, while the conversion rules enhance the safety of the library compared to the libraries that do not model quantity kinds.
The same rules propagate to derived quantities. For example, we can define strongly typed horizontal length and area:
inline constexpr struct horizontal_length : quantity_spec<isq::length> {} horizontal_length;
inline constexpr struct horizontal_area : quantity_spec<isq::area, horizontal_length * isq::width> {} horizontal_area;The first definition says that a
horizontal_length is a more
specialized quantity than
isq::length
and belongs to the same quantity kind. The second line defines a
horizontal_area, which is a more
specialized quantity than
isq::area,
so it has a more constrained recipe as well. Thanks to that:
static_assert(implicitly_convertible(horizontal_length, isq::length));
static_assert(!implicitly_convertible(isq::length, horizontal_length));
static_assert(explicitly_convertible(isq::length, horizontal_length));
static_assert(implicitly_convertible(horizontal_area, isq::area));
static_assert(!implicitly_convertible(isq::area, horizontal_area));
static_assert(explicitly_convertible(isq::area, horizontal_area));
static_assert(implicitly_convertible(isq::length * isq::length, isq::area));
static_assert(!implicitly_convertible(isq::length * isq::length, horizontal_area));
static_assert(explicitly_convertible(isq::length * isq::length, horizontal_area));
static_assert(implicitly_convertible(horizontal_length * isq::width, isq::area));
static_assert(implicitly_convertible(horizontal_length * isq::width, horizontal_area));Unfortunately, derived quantity equations often do not automatically form a hierarchy tree. This is why sometimes it is not obvious what such a tree should look like. Also, the [ISO/IEC Guide 99] explicitly states:
The division of ‘quantity’ according to ‘kind of quantity’ is, to some extent, arbitrary.
The below presents some arbitrary hierarchy of derived quantities of kind energy:
Notice, that even though all of those quantities have the same dimension and can be expressed in the same units, they have different quantity equations used to create them implicitly:
energy is the most generic
one and thus can be created from base quantities of
mass,
length, and
time. As those are also the roots of
quantities of their kinds and all other quantities are implicitly
convertible to them, it means that an
energy can be implicitly constructed
from any quantity having proper powers of mass,
length, and time.
static_assert(implicitly_convertible(isq::mass * pow<2>(isq::length) / pow<2>(isq::time), isq::energy));
static_assert(implicitly_convertible(isq::mass * pow<2>(isq::height) / pow<2>(isq::time), isq::energy));mechanical_energy is a more
“specialized” quantity than energy
(not every energy is a
mechanical_energy). It is why an
explicit cast is needed to convert from either
energy or the results of its
quantity equation.
static_assert(!implicitly_convertible(isq::energy, isq::mechanical_energy));
static_assert(explicitly_convertible(isq::energy, isq::mechanical_energy));
static_assert(!implicitly_convertible(isq::mass * pow<2>(isq::length) / pow<2>(isq::time), isq::mechanical_energy));
static_assert(explicitly_convertible(isq::mass * pow<2>(isq::length) / pow<2>(isq::time), isq::mechanical_energy));gravitational_potential_energy
is not only even more specialized one but additionally, it is special in
a way that it provides its own “constrained” quantity equation. Maybe
not every mass * pow<2>(length) / pow<2>(time)
is a gravitational_potential_energy,
but every mass * acceleration_of_free_fall * height
is.
static_assert(!implicitly_convertible(isq::energy, gravitational_potential_energy));
static_assert(explicitly_convertible(isq::energy, gravitational_potential_energy));
static_assert(!implicitly_convertible(isq::mass * pow<2>(isq::length) / pow<2>(isq::time), gravitational_potential_energy));
static_assert(explicitly_convertible(isq::mass * pow<2>(isq::length) / pow<2>(isq::time), gravitational_potential_energy));
static_assert(implicitly_convertible(isq::mass * isq::acceleration_of_free_fall * isq::height, gravitational_potential_energy));In the physical units library, we also need an abstraction describing an entire family of quantities of the same kind. Such quantities have not only the same dimension but also can be expressed in the same units.
To annotate a quantity to represent its kind (and not just a
hierarchy tree’s root quantity), we introduced a kind_of<>
specifier. For example, to express any quantity of length, we
need to specify kind_of<isq::length>.
That entity behaves as any quantity of its kind. This means that it is
implicitly convertible to any quantity in a tree:
static_assert(!implicitly_convertible(isq::length, isq::height));
static_assert(implicitly_convertible(kind_of<isq::length>, isq::height));Additionally, the result of operations on quantity kinds is also a quantity kind:
static_assert(same_type<kind_of<isq::length> / kind_of<isq::time>, kind_of<isq::length / isq::time>>);However, if at least one equation’s operand is not a quantity kind, the result becomes a “strong” quantity where all the kinds are converted to the hierarchy tree’s root quantities:
static_assert(!same_type<kind_of<isq::length> / isq::time, kind_of<isq::length / isq::time>>);
static_assert(same_type<kind_of<isq::length> / isq::time, isq::length / isq::time>);Please note that only a root quantity from the hierarchy tree or the
one marked with is_kind specifier in
the quantity_spec definition can be
put as a template parameter to the
kind_of specifier. For example,
kind_of<isq::width>
will fail to compile. However, we can call get_kind(q)
to obtain a kind of any quantity:
static_assert(get_kind(isq::width) == kind_of<isq::length>);is_kindDimension-based type safety prevents many errors, but quantities may
share the same dimension while representing fundamentally incompatible
physical concepts. The is_kind
specifier creates distinct quantity types within a hierarchy that cannot
be mixed despite sharing dimension and parent quantity properties.
The is_kind specifier addresses
cases where multiple incompatible concepts must share a parent
quantity’s properties (unit, quantity type) while remaining isolated
from each other. This is necessary when quantities cannot be
meaningfully added or compared without explicit conversion, yet derive
from the same physical basis.
The is_kind specifier creates
subkinds within an existing quantity hierarchy tree, not independent
trees. Subkinds inherit properties from their parent:
For completely independent quantities with different dimension trees, separate root quantities should be defined instead (e.g., frequency and activity are independent roots, not subkinds).
Examples:
A distinct quantity kind is defined by adding
is_kind to the
quantity_spec definition:
inline constexpr struct fluid_head : quantity_spec<isq::height, is_kind> {} fluid_head;
inline constexpr struct water_head : quantity_spec<isq::height, is_kind> {} water_head;Both fluid_head and
water_head are subkinds of
height (inheriting dimension of length and unit of
metre), but is_kind makes them
distinct incompatible kinds requiring explicit conversion.
Quantities marked with is_kind
enforce strict type boundaries:
No implicit or explicit conversion between different kinds:
static_assert(!implicitly_convertible(fluid_head, water_head));
static_assert(!explicitly_convertible(fluid_head, water_head));
static_assert(!castable(fluid_head, water_head));No arithmetic operations or comparisons between different kinds:
quantity h_fluid = fluid_head(2 * m);
quantity h_water = water_head(10 * m);
// auto sum = h_fluid + h_water; // Compile-time error
// bool cmp = h_fluid < h_water; // Compile-time errorExplicit conversion to base quantity required for generic operations:
quantity h1 = isq::height(h_fluid);
quantity h2 = isq::height(h_water);
quantity sum = h1 + h2; // OK: both are isq::heightNote: Implicit conversion from
is_kind quantities to their base is
not allowed:
quantity<isq::height[m]> h = h_fluid; // Compile-time errorCompatible with kind_of
introspection:
static_assert(get_kind(fluid_head) == kind_of<fluid_head>);
static_assert(get_kind(water_head) == kind_of<water_head>);
static_assert(get_kind(isq::height) == kind_of<isq::length>);
static_assert(get_kind(fluid_head) != get_kind(water_head));
static_assert(get_kind(fluid_head) != get_kind(isq::height));Modeling a system of units is the most important feature and a selling point of every physical units library. Thanks to that, the library can protect users from performing invalid operations on quantities and provide automated conversion factors between various compatible units.
Probably all the libraries in the wild model the [SI] or at least most of it (refer to SI units of quantities of the same dimension but different kinds for more details) and many of them provide support for additional units belonging to various other systems (e.g., imperial).
Systems of quantities specify a set of quantities and equations relating to those quantities. Those equations do not take any unit or a numerical representation into account at all. In order to create a quantity, we need to add those missing pieces of information. This is where a system of units kicks in.
The [SI] is explicitly stated to be based on the ISQ. Among others, it defines seven base units, one for each base quantity. In the library, this is expressed by associating a quantity kind to a unit being defined:
inline constexpr struct metre : named_unit<"m", kind_of<isq::length>> {} metre;The kind_of<isq::length>
above states explicitly that this unit has an associated quantity kind.
In other words,
si::metre
(and scaled units based on it) can be used to express the amount of any
quantity of kind length.
One of the strongest points of the [SI] system is that its units compose. This allows providing thousands of different units for hundreds of various quantities with a really small set of predefined units and prefixes. For example, one can write:
quantity<si::metre / si::second> q;to express a quantity of speed. The resulting quantity type is implicitly inferred from the unit equation by repeating exactly the same operations on the associated quantity kinds.
As units are regular values, we can easily provide a helper ad-hoc unit with:
constexpr auto mps = si::metre / si::second;
quantity<mps> q;The [SI] provides the names for 22 common coherent units of 22 derived quantities.
Each such named derived unit is a result of a specific predefined unit equation. For example, a unit of power quantity is defined as:
inline constexpr struct watt : named_unit<"W", joule / second> {} watt;However, a power quantity can be expressed in other units as well. For example, the following:
auto q1 = 42 * W;
std::cout << q1 << "\n";
std::cout << q1.in(J / s) << "\n";
std::cout << q1.in(N * m / s) << "\n";
std::cout << q1.in(kg * m2 / s3) << "\n";prints:
42 W
42 J/s
42 N m/s
42 kg m²/s³
All of the above quantities are equivalent and mean exactly the same.
Some derived units are valid only for specific derived quantities. For example, [SI] specifies both hertz and becquerel derived units with the same unit equation \(s^{-1}\). However, it also explicitly states:
The hertz shall only be used for periodic phenomena and the becquerel shall only be used for stochastic processes in activity referred to a radionuclide.
This is why it is important for the library to allow constraining such units to be used only with a specific quantity kind:
inline constexpr struct hertz : named_unit<"Hz", one / second, kind_of<isq::frequency>> {} hertz;
inline constexpr struct becquerel : named_unit<"Bq", one / second, kind_of<isq::activity>> {} becquerel;With the above, hertz can only be
used for frequencies, while
becquerel should only be used for
quantities of activity. This means that the following equation
will not compile, improving the type-safety of the library:
auto q = 1 * Hz + 1 * Bq; // Fails to compileBesides named units, the SI specifies also 24 prefixes
(all being a power of
10) that can
be prepended to all named units to obtain various scaled versions of
them.
Implementation of
std::ratio
provided by all major compilers is able to express only 16 of them. This
is why, we had to find an alternative way to represent a unit’s
magnitude in a more flexible way.
Each prefix is implemented as:
template<PrefixableUnit U> struct quecto_ : prefixed_unit<"q", mag_power<10, -30>, U{}> {};
template<PrefixableUnit auto U> constexpr quecto_<decltype(U)> quecto;and then a unit can be prefixed in the following way:
inline constexpr auto qm = quecto<metre>;The usage of mag_power not only
enables providing support for SI prefixes, but it can also efficiently
represent any rational magnitude. For example, [ISO/IEC 80000] (part 13) prefixes used
in the IT industry can be implemented as:
template<PrefixableUnit U> struct yobi_ : prefixed_unit<"Yi", mag_power<2, 80>, U{}> {};
template<PrefixableUnit auto U> constexpr yobi_<decltype(U)> yobi;Please note that to improve the readability of generated types that are exposed in compiler errors and debugger, the variable template takes an NTTP and converts it to its type before passing the argument to the associated class template.
In the [SI], all units are either base or derived units or prefixed versions of those. However, those are not the only options possible.
For example, there is a list of off-system units accepted for use with [SI]. All of those are scaled versions of the [SI] units with ratios that can’t be explicitly expressed with predefined SI prefixes. Those include units like minute, hour, or electronvolt:
inline constexpr struct minute : named_unit<"min", mag<60> * si::second> {} minute;
inline constexpr struct hour : named_unit<"h", mag<60> * minute> {} hour;
inline constexpr struct electronvolt : named_unit<"eV",
mag_ratio<1'602'176'634, 1'000'000'000> * mag_power<10, -19> * si::joule> {} electronvolt;Also, units of other systems of units are often defined in terms of scaled versions of other (often SI) units. For example, the international yard is defined as:
inline constexpr struct yard : named_unit<"yd", mag_ratio<9'144, 10'000> * si::metre> {} yard;and then a foot can be defined
as:
inline constexpr struct foot : named_unit<"ft", mag_ratio<1, 3> * yard> {} foot;For some units, a magnitude might also be irrational. The best
example here is a degree which is
defined using a floating-point magnitude having a factor of the number π
(Pi):
inline constexpr struct pi_c : mag_constant<{u8"π" /* U+03C0 GREEK SMALL LETTER PI */, "pi"}, std::numbers::pi_v<long double>> {} pi_c;
inline constexpr struct pi : named_constant<symbol_text{u8"π" /* U+03C0 GREEK SMALL LETTER PI */, "pi"}, mag<pi_c> * one> {} pi;
inline constexpr auto π /* U+03C0 GREEK SMALL LETTER PI */ = pi;inline constexpr struct degree : named_unit<{u8"°", "deg"}, mag_ratio<1, 180> * π * si::radian> {} degree;Adding, subtracting, or comparing 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 other, 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 operation 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:
quantity q1 = 1 * km + 1 * mi; // quantity<common_unit<international::mile, si::kilo_<si::metre>>{}, int>
quantity q2 = 1. * rad + 1. * deg; // quantity<common_unit<si::degree, si::radian>{}, double>The above is to not privilege any unit in the library:
1 * mi + 1 * nmi
computation to m because
m could be the privileged SI base
unit),1 * m + 1 * cm
computation to m because
m is the privileged SI base
unit).Please note, that a user should never explicitly instantiate a
common_unit class template. The
library’s framework will do it based on the provided quantity
equation.
Units are available via their full names or through their short symbols. To use a long version, it is enough to type:
quantity q1 = 42 * si::metre / si::second;
quantity q2 = 42 * si::kilo<si::metre> / si::hour;To simplify how we spell it a short, user-friendly symbols are provided in a dedicated subnamespace in systems definitions:
namespace si::unit_symbols {
constexpr auto m = si::metre;
constexpr auto km = si::kilo<si::metre>;
constexpr auto s = si::second;
constexpr auto h = si::hour;
}Unit symbols introduce a lot of short identifiers into the current
namespace. This is why they are opt-in. A user has to explicitly
“import” them from a dedicated
unit_symbols namespace:
using namespace si::unit_symbols;
quantity q1 = 42 * m / s;
quantity q2 = 42 * km / h;or:
using si::unit_symbols::m;
using si::unit_symbols::km;
using si::unit_symbols::s;
using si::unit_symbols::h;
quantity q1 = 42 * m / s;
quantity q2 = 42 * km / h;Thanks to [P1949R7] we also provide alternative object identifiers using Unicode characters in their names for most unit symbols. The code using Unicode looks nicer, but it is harder to type on the keyboard. This is why we provide both versions of identifiers for such units.
Portable only
|
With Unicode characters
|
|---|---|
|
|
It is worth noting that not all such units may get Unicode identifiers. Some of them do not have the XID_Start property. For example:
A quantity value contains a numerical value and a unit. Both of them may have various text representations. Not only numbers but also units can be formatted in many different ways. Additionally, every dimension can be represented as a text as well.
This chapter will discuss the different options we have here.
Note: For now, there is no standardized way to handle formatted text input in the C++ standard library, so this paper does not propose any approach to convert text to quantities. If [P1729R3] will be accepted by the LEWG, then we will add a proper “Text input” chapter as well.
The definitions of dimensions, units, prefixes, and constants require unique text symbols to be assigned for each entity. Those symbols can be composed to express dimensions and units of base and derived quantities.
Note: The below code examples are based on the latest version of the [mp-units] library and might not be the final version proposed for standardization.
Dimensions:
inline constexpr struct dim_length : base_dimension<"L"> {} dim_length;
inline constexpr struct dim_mass : base_dimension<"M"> {} dim_mass;
inline constexpr struct dim_time : base_dimension<"T"> {} dim_time;
inline constexpr struct dim_electric_current : base_dimension<"I"> {} dim_electric_current;
inline constexpr struct dim_thermodynamic_temperature : base_dimension<{u8"Θ", "O"}> {} dim_thermodynamic_temperature;
inline constexpr struct dim_amount_of_substance : base_dimension<"N"> {} dim_amount_of_substance;
inline constexpr struct dim_luminous_intensity : base_dimension<"J"> {} dim_luminous_intensity;Units:
inline constexpr struct second : named_unit<"s", kind_of<isq::time>> {} second;
inline constexpr struct metre : named_unit<"m", kind_of<isq::length>> {} metre;
inline constexpr struct gram : named_unit<"g", kind_of<isq::mass>> {} gram;
inline constexpr auto kilogram = kilo<gram>;
inline constexpr struct newton : named_unit<"N", kilogram * metre / square(second)> {} newton;
inline constexpr struct joule : named_unit<"J", newton * metre> {} joule;
inline constexpr struct watt : named_unit<"W", joule / second> {} watt;
inline constexpr struct coulomb : named_unit<"C", ampere * second> {} coulomb;
inline constexpr struct volt : named_unit<"V", watt / ampere> {} volt;
inline constexpr struct farad : named_unit<"F", coulomb / volt> {} farad;
inline constexpr struct ohm : named_unit<{u8"Ω", "ohm"}, volt / ampere> {} ohm;Prefixes:
template<PrefixableUnit U> struct micro_ : prefixed_unit<{u8"µ", "u"}, mag_power<10, -6>, U{}> {};
template<PrefixableUnit U> struct milli_ : prefixed_unit<"m", mag_power<10, -3>, U{}> {};
template<PrefixableUnit U> struct centi_ : prefixed_unit<"c", mag_power<10, -2>, U{}> {};
template<PrefixableUnit U> struct deci_ : prefixed_unit<"d", mag_power<10, -1>, U{}> {};
template<PrefixableUnit U> struct deca_ : prefixed_unit<"da", mag_power<10, 1>, U{}> {};
template<PrefixableUnit U> struct hecto_ : prefixed_unit<"h", mag_power<10, 2>, U{}> {};
template<PrefixableUnit U> struct kilo_ : prefixed_unit<"k", mag_power<10, 3>, U{}> {};
template<PrefixableUnit U> struct mega_ : prefixed_unit<"M", mag_power<10, 6>, U{}> {};Constants:
inline constexpr struct hyperfine_structure_transition_frequency_of_cs :
named_constant<{u8"Δν_Cs", "dv_Cs"}, mag<9'192'631'770> * hertz> {} hyperfine_structure_transition_frequency_of_cs;
inline constexpr struct speed_of_light_in_vacuum :
named_constant<"c", mag<299'792'458> * metre / second> {} speed_of_light_in_vacuum;
inline constexpr struct planck_constant :
named_constant<"h", mag_ratio<662'607'015, 100'000'000> * mag_power<10, -34> * joule * second> {} planck_constant;
inline constexpr struct elementary_charge :
named_constant<"e", mag_ratio<1'602'176'634, 1'000'000'000> * mag_power<10, -19> * coulomb> {} elementary_charge;
inline constexpr struct boltzmann_constant :
named_constant<"k", mag_ratio<1'380'649, 1'000'000> * mag_power<10, -23> * joule / kelvin> {} boltzmann_constant;
inline constexpr struct avogadro_constant :
named_constant<"N_A", mag_ratio<602'214'076, 100'000'000> * mag_power<10, 23> / mole> {} avogadro_constant;
inline constexpr struct luminous_efficacy :
named_constant<"K_cd", mag<683> * lumen / watt> {} luminous_efficacy;Note: Two symbols always have to be provided if the primary symbol contains characters outside of the basic literal character set. The first must be provided as a UTF-8 literal and may contain any Unicode characters. The second one must provide an alternative spelling and only use characters from within of basic literal character set.
Unicode provides only a minimal set of characters available as
subscripts, which are often used to differentiate various constants and
quantities of the same kind. To workaround this issue, [mp-units] uses
'_'
character to specify that the following characters should be considered
a subscript of the symbol.
Although the ISQ defined in [ISO/IEC 80000] provides symbols for each quantity type, there is little use for them in the C++ code. In the [mp-units] project, we never had a request to provide such symbol definitions. Even though having them for completeness could be nice, they seem to not be required by the domain experts for their daily jobs. Also, it is worth noting that providing those raises some additional standardization and implementation challenges.
If we decide to provide symbols, the rest of this chapter provides the domain information to assess the complexity and potential issues with standardization and implementation of those.
All ISQ quantities have an official symbol assigned in their definitions, and how those should be printed is exactly specified. [ISO/IEC 80000] explicitly states:
The quantity symbols shall be written in italic (sloping) type, irrespective of the type used in the rest of the text.
Additionally, [ISO/IEC 80000] provides additional requirements for printing quantities of vector and tensor characters:
Note: In the above examples, the second symbol with arrows above should also use letters written in italics. The author could not find a way to format it properly in this document.
There are also a few requirements for printing subscripts of quantity types. [ISO/IEC 80000] states:
The following principles for the printing of subscripts apply:
- A subscript that represents a physical quantity or a mathematical variable, such as a running number, is printed in italic (sloping) type.
- Other subscripts, such as those representing words or fixed numbers, are printed in roman (upright) type.
It is worth noting that only a limited set of Unicode characters are available as subscripts. Those are often used to differentiate various quantities of the same kind.
For example, it is impossible to encode the symbols of the following quantities:
It is important to state that the same issues are related to constant
definitions. For them, in the Symbol definition examples
chapter, we proposed to use the
'_'
character instead, as stated in Lack of Unicode subscript
characters. We could use the same practice here.
Another challenge here might be related to the fact that [ISO/IEC 80000] often provides more than one symbol for the same quantity. For example:
Last but not least, it is worth noting that symbols of ISQ base quantities are not necessary the same as official dimension symbols of those quantities:
Quantity type
|
Quantity type symbol
|
Dimension symbol
|
|---|---|---|
| length | l, L | L |
| mass | m | M |
| time | t | T |
| electric current | I, i | I |
| thermodynamic temperature | T, Θ | Θ |
| amount of substance | n(X) | N |
| luminous intensity | Iv, (I) | J |
Founding a way to define, use, and print named quantity types is not enough. What should also be covered here is the text output of derived quantities. There are plenty of operations that one might do on scalar, vector, and tensor quantities, and all of them result in another quantity type, which should also be able to be printed in the console text output.
Taking all the challenges and issues mentioned above, we do not propose providing quantity type symbols in their definitions and any text input/output support for those.
fixed_stringAs shown above, symbols are provided as class NTTPs in the library. This means that the string type used for such a purpose has to satisfy the structural type requirements of the C++ language. One of such requirements is to expose all the data members publicly. So far, none of the existing string types in the C++ standard library satisfies such requirements. This is why we need to introduce a new type.
Such type should:
Such a type does not need to expose a string-like interface. In case
its interface is immutable, we can easily wrap it with std::string_view
to get such an interface for free.
This type is being proposed separately in [P3094R6].
symbol_textMany symbols of units, prefixes, and constants require using a Unicode character set. For example:
The library should provide such Unicode output by default to be consistent with official systems’ specifications.
On the other hand, plenty of terminals do not support Unicode characters. Also, general engineering experience shows that people often prefer to work with a basic literal character set. This is why all such entities should provide an alternative spelling in their definitions.
This is where symbol_text comes
into play. It is a simple wrapper over the two
fixed_string objects:
template<std::size_t N, std::size_t M>
class symbol_text {
public:
fixed_u8string<N> utf8_; // exposition only
fixed_string<M> portable_; // exposition only
constexpr explicit(false) symbol_text(char ch);
consteval explicit(false) symbol_text(const char (&txt)[N + 1]);
constexpr explicit(false) symbol_text(const fixed_string<N>& txt);
consteval symbol_text(const char8_t (&u)[N + 1], const char (&a)[M + 1]);
constexpr symbol_text(const fixed_u8string<N>& u, const fixed_string<M>& a);
constexpr const auto& utf8() const;
constexpr const auto& portable() const;
constexpr bool empty() const;
template<std::size_t N2, std::size_t M2>
constexpr friend symbol_text<N + N2, M + M2> operator+(const symbol_text& lhs, const symbol_text<N2, M2>& rhs);
template<std::size_t N2, std::size_t M2>
friend constexpr auto operator<=>(const symbol_text& lhs, const symbol_text<N2, M2>& rhs) noexcept;
template<std::size_t N2, std::size_t M2>
friend constexpr bool operator==(const symbol_text& lhs, const symbol_text<N2, M2>& rhs) noexcept;
};
symbol_text(char) -> symbol_text<1, 1>;
template<std::size_t N>
symbol_text(const char (&)[N]) -> symbol_text<N - 1, N - 1>;
template<std::size_t N>
symbol_text(const fixed_string<N>&) -> symbol_text<N, N>;
template<std::size_t N, std::size_t M>
symbol_text(const char8_t (&)[N], const char (&)[M]) -> symbol_text<N - 1, M - 1>;
template<std::size_t N, std::size_t M>
symbol_text(const fixed_u8string<N>&, const fixed_string<M>&) -> symbol_text<N, M>;It is important to note that the
utf8_ text representation is used
only when the output is of either:
char8_t
type,char and
std::text_encoding::literal().mib() == std::text_encoding::id::UTF8.Otherwise, portable_ is used.
character_setISQ and [SI] standards always specify symbols using Unicode encoding. This is why it is a default and primary target for text output. However, in some applications or environments, a standard portable text output using only the characters from the basic literal character set can be preferred by users.
This is why the library provides an option to change the default encoding to the portable one with:
enum class character_set : std::int8_t {
utf8, // µs; m³; L²MT⁻³
portable, // us; m^3; L^2MT^-3
default_encoding = utf8
};dimension_symbol_formattingdimension_symbol_formatting is a
data type describing the configuration of the symbol generation
algorithm.
struct dimension_symbol_formatting {
character_set char_set = character_set::default_encoding;
};dimension_symbol()Returns a std::string_view
with the symbol of a dimension for the provided configuration:
template<dimension_symbol_formatting fmt = dimension_symbol_formatting{}, typename CharT = char, Dimension D>
consteval std::string_view dimension_symbol(D);Note: It could be refactored to dimension_symbol(D, fmt)
when [P1045R1] is available.
For example:
static_assert(dimension_symbol<{.char_set = character_set::portable}>(isq::power.dimension) == "L^2MT^-3");dimension_symbol_to()Inserts the generated dimension symbol into the output text iterator at runtime.
template<typename CharT = char, std::output_iterator<CharT> Out, Dimension D>
constexpr Out dimension_symbol_to(Out out, D d, dimension_symbol_formatting fmt = dimension_symbol_formatting{});For example:
std::string txt;
dimension_symbol_to(std::back_inserter(txt), isq::power.dimension, {.char_set = character_set::portable});
std::cout << txt << "\n";The above prints:
L^2MT^-3
unit_symbol_formattingunit_symbol_formatting is a data
type describing the configuration of the symbol generation algorithm. It
contains three orthogonal fields, each with a default value.
enum class unit_symbol_solidus : std::int8_t {
one_denominator, // m/s; kg m⁻¹ s⁻¹
always, // m/s; kg/(m s)
never, // m s⁻¹; kg m⁻¹ s⁻¹
default_solidus = one_denominator
};
enum class unit_symbol_separator : std::int8_t {
space, // kg m²/s²
half_high_dot, // kg⋅m²/s² (valid only for Unicode encoding)
default_separator = space
};
struct unit_symbol_formatting {
character_set char_set = character_set::default_encoding;
unit_symbol_solidus solidus = unit_symbol_solidus::default_solidus;
unit_symbol_separator separator = unit_symbol_separator::default_separator;
};unit_symbol_solidus impacts how
the division of unit symbols is being presented in the text output. By
default, the ‘/’ will be printed if only one unit component is in the
denominator. Otherwise, the exponent syntax will be used.
unit_symbol_separator specifies
how multiple multiplied units should be separated from each other. By
default, the space (’ ’) will be used as a separator.
unit_symbol()Returns a std::string_view
with the symbol of a unit for the provided configuration:
template<unit_symbol_formatting fmt = unit_symbol_formatting{}, typename CharT = char, Unit U>
consteval std::string_view unit_symbol(U);Note: It could be refactored to unit_symbol(U, fmt)
when [P1045R1] is available.
For example:
static_assert(unit_symbol<{.solidus = unit_symbol_solidus::never,
.separator = unit_symbol_separator::half_high_dot}>(kg * m / s2) == "kg⋅m⋅s⁻²");unit_symbol_to()Inserts the generated unit symbol into the output text iterator at runtime.
template<typename CharT = char, std::output_iterator<CharT> Out, Unit U>
constexpr Out unit_symbol_to(Out out, U u, unit_symbol_formatting fmt = unit_symbol_formatting{});For example:
std::string txt;
unit_symbol_to(std::back_inserter(txt), kg * m / s2,
{.solidus = unit_symbol_solidus::never, .separator = unit_symbol_separator::half_high_dot});
std::cout << txt << "\n";The above prints:
kg⋅m⋅s⁻²
Here are a few examples of scaled unit text output in action:
inline constexpr Unit auto my_unit_1 = mag_ratio<1, 4> * si::second;
inline constexpr Unit auto my_unit_2 = mag_power<10, 4> * si::metre;
inline constexpr Unit auto my_unit_3 = mag<25> * mag_power<10, 4> * si::metre;
std::cout << 100 * my_unit_1 << "\n";
std::cout << 100 * my_unit_2 << "\n";
std::cout << 100 * my_unit_3 << "\n";The above prints:
100 (1/4 s)
100 (10⁴ m)
100 (25 × 10⁴ m)
As we can see a scaled unit has a magnitude and a reference unit. To
denote the scope of such a unit, we currently enclose it in
(...).
In most cases scaled units are hidden behind named units so the above outputs are a bit artifical. However, there are a few real-life where a user directly faces a scaled unit. For example:
inline constexpr Unit auto L_per_100km = L / (mag<100> * km);The above is a derived unit of litre divided by a scaled unit of
100
kilometers. For example, the following:
std::cout << 6.7 * L_per_100km << "\n";prints:
6.7 L/(100 km)
The current output of the fuel consumption unit is only one of the
options here. It favors a derived unit over a scaled unit (i.e., the
resulting type is derived_unit<non_si::litre, per<scaled_unit<{some magnitude representing 100}, si::kilo<si::metre>>>>).
Another option would be to prefer a scaled unit so the result would be
scaled_unit<{some magnitude representing 1/100}, derived_unit<non_si::litre, per<si::kilo<si::metre>>>
and the output would look like:
6.7 (1/100 L/km)
The output could also look like this:
6.7 × 10⁻² L/km
but this, even though it is mathematically correct, is the poorest to
express the intent here. The unit we use daily is the number of liters
consumed for
100 km, and
not for a single km, which the last
output suggests (i.e., “6.7 hundredths of a liter per kilometer”).
This is why we initially proposed the first version. However, on one of the meetings it was brought that this approach also leads to some issues in case of other quantities.
According to the current rules, the following code:
std::cout << 10 * L_per_100km * (20 * km) << "\n";prints the following output:
200 L km/(100 km)
At least for now, the units do not simplify which may look suprising.
Another thing worth noting here is that in case a value of a
numerator or denumerator is greater or equal
1000 we use
an exponential notation:
inline constexpr Unit auto L_per_1000km = L / (mag<1000> * km);
std::cout << 10 * L_per_1000km << "\n";prints:
10 L/(10³ km)
A motivation for that is that typically, for a value of
1000 or
greater, a user could use a larger SI prefix (even though it would not
make sense for kilometers).
All of the above are the inputs for a discussion and we are open to fine-tuning this behavior according to the LEWG guidelines.
Some common units expressed with a specialization of the
common_unit class template need
special printing rules for their symbols. As they represent a minimum
set of equivalent common units resulting from the addition or
subtraction of multiple quantities, we print all of them as a scaled
version of the source unit. 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";
std::cout << 1. * rad + 1. * deg << "\n";prints:
40771 [(1/25146 mi), (1/15625 km)]
108167 [(1/50292 mi), (1/57875 nmi)]
23 [(1/5 km/h), (1/18 m/s)]
183.142 [(1/π°), (1/180 rad)]
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.
It is important to note that this output is provided only for intermediate results of the equations, as shown above. A user usually knows which unit should be used, and explicit conversion can be made to achieve that. For example:
std::cout << (1 * km + 1 * mi).in<double>(km) << "\n";prints:
2.60934 km
Library’s framework requires some Unicode characters for text output. The below table lists all of them together with the recommended portable replacements:
Name
|
Symbol
|
C++ code
|
Portable alternative
|
|---|---|---|---|
| SUPERSCRIPT ZERO | ⁰ | u8"\u2070" |
"0" |
| SUPERSCRIPT ONE | ¹ | u8"\u00b9" |
"1" |
| SUPERSCRIPT TWO | ² | u8"\u00b2" |
"2" |
| SUPERSCRIPT THREE | ³ | u8"\u00b3" |
"3" |
| SUPERSCRIPT FOUR | ⁴ | u8"\u2074" |
"4" |
| SUPERSCRIPT FIVE | ⁵ | u8"\u2075" |
"5" |
| SUPERSCRIPT SIX | ⁶ | u8"\u2076" |
"6" |
| SUPERSCRIPT SEVEN | ⁷ | u8"\u2077" |
"7" |
| SUPERSCRIPT EIGHT | ⁸ | u8"\u2078" |
"8" |
| SUPERSCRIPT NINE | ⁹ | u8"\u2079" |
"9" |
| SUPERSCRIPT MINUS | ⁻ | u8"\u207b" |
"-" |
| MULTIPLICATION SIGN | × | u8"\u00d7" |
"x" |
| GREEK SMALL LETTER PI | π | u8"\u03c0" |
"pi" |
| DOT OPERATOR | ⋅ | u8"\u22C5" |
<none>1 |
Here is an example of how the above are being used in a code:
static_assert(unit_symbol(kilogram * metre / square(second)) == "kg m/s²");
static_assert(unit_symbol<usf{.separator = half_high_dot}>(kilogram * metre / square(second)) == "kg⋅m/s²");
static_assert(unit_symbol<usf{.char_set = portable}>(kilogram * metre / square(second)) == "kg m/s^2");
static_assert(unit_symbol<usf{.solidus = never}>(kilogram * metre / square(second)) == "kg m s⁻²");
static_assert(unit_symbol<usf{.char_set = portable, .solidus = never}>(kilogram * metre / square(second)) == "kg m s^-2");
static_assert(unit_symbol(mag_ratio<1, 18000> * metre / second) == "[1/18 × 10⁻³ m]/s");
static_assert(unit_symbol<usf{.char_set = portable}>(mag_ratio<1, 18000> * metre / second) == "[1/18 x 10^-3 m]/s");
static_assert(unit_symbol(mag<1> / (mag<2> * mag<pi_c>)*metre) == "[2⁻¹ π⁻¹ m]");
static_assert(unit_symbol<usf{.solidus = always}>(mag<1> / (mag<2> * mag<pi_c>)*metre) == "[1/(2 π) m]");
static_assert(unit_symbol<usf{.char_set = portable, .solidus = always}>(mag<1> / (mag<2> * mag<pi_c>)*metre) == "[1/(2 pi) m]");Additionally, if we decide to provide
per_mille unit together with the
framework (next to one,
percent, and
parts_per_million) we will need a
symbol for it as well:
Name
|
Symbol
|
C++ code
|
Portable alternative
|
|---|---|---|---|
| PER MILLE SIGN | ‰ | u8”030” | ??? |
There is no good choice for a portable replacement here. We may try to be brief and use “%o” (Wikipedia uses this symbol as a redirect for per mille) or provide a longer textual string. The problem is that there is more than one option to chose from here (names mentioned in Wikipedia):
space_before_unit_symbol
customization pointThe [SI] says:
The numerical value always precedes the unit and a space is always used to separate the unit from the number. … The only exceptions to this rule are for the unit symbols for degree, minute and second for plane angle,
°,′and″, respectively, for which no space is left between the numerical value and the unit symbol.
There are more units with such properties. For example, per
mille(‰).
To support the above, the library exposes
space_before_unit_symbol
customization point. By default, its value is
true for all
the units. This means that a number and a unit will be separated by the
space in the output text. To change this behavior, a user should provide
a explicit specialization for a specific unit:
template<>
constexpr bool space_before_unit_symbol<non_si::degree> = false;The above works only for the default formatting or for the format
strings that use
%? placement
field (std::format("{}", q)
is equivalent to std::format("{:%N%?%U}", q)).
In case a user provides custom format specification (e.g., std::format("{:%N %U}", q)),
the library will always obey this specification for all the units (no
matter what the actual value of the
space_before_unit_symbol
customization point is) and the separating space will always be used in
this case.
The easiest way to print a dimension, unit, or quantity is to provide its object to the output stream:
const quantity v1 = avg_speed(220. * km, 2 * h);
const quantity v2 = avg_speed(140. * mi, 2 * h);
std::cout << v1 << '\n'; // 110 km/h
std::cout << v2 << '\n'; // 70 mi/h
std::cout << v2.unit << '\n'; // mi/h
std::cout << v2.dimension << '\n'; // LT⁻¹The text output will always print the value using the default formatting for this entity.
Only basic formatting can be applied for output streams. It includes control over width, fill, and alignment.
The numerical value of the quantity will be printed according to the current stream state and standard manipulators may be used to customize that (assuming that the underlying representation type respects them).
std::cout << "|" << std::setw(10) << 123 * m << "|\n"; // | 123 m|
std::cout << "|" << std::setw(10) << std::left << 123 * m << "|\n"; // |123 m |
std::cout << "|" << std::setw(10) << std::setfill('*') << 123 * m << "|\n"; // |123 m*****|Detailed formatting of any entity may be obtained with std::format()
usage and then provided to the stream output if needed.
Note: Custom stream manipulators may be provided to control a dimension and unit symbol output if requested by WG21.
The library provides custom formatters for
std::format
facility, which allows fine-grained control over what and how it is
being printed in the text output.
Formatting grammar for all the entities provides control over width,
fill, and alignment. The C++ standard grammar tokens fill-and-align
and width are being used. They treat
the entity as a contiguous text to be aligned. For example, here are a
few examples of the quantity numerical value and symbol formatting:
std::println("|{:0}|", 123 * m); // |123 m|
std::println("|{:10}|", 123 * m); // | 123 m|
std::println("|{:<10}|", 123 * m); // |123 m |
std::println("|{:>10}|", 123 * m); // | 123 m|
std::println("|{:^10}|", 123 * m); // | 123 m |
std::println("|{:*<10}|", 123 * m); // |123 m*****|
std::println("|{:*>10}|", 123 * m); // |*****123 m|
std::println("|{:*^10}|", 123 * m); // |**123 m***|It is important to note that in the second line above, the quantity text is aligned to the right by default, which is consistent with the formatting of numeric types. Units and dimensions behave as text and, thus, are aligned to the left by default.
dimension-format-spec = [fill-and-align], [width], [dimension-spec];
dimension-spec = [character-set];
character-set = 'U' | 'P';
In the above grammar:
fill-and-align
and width tokens are defined in the
28.5.2.2
[format.string.std]
chapter of the C++ standard specification,character-set
token specifies the symbol text encoding:
U (default) uses the
UTF-8 symbols defined by [ISO/IEC 80000] (e.g.,
LT⁻²),P forces non-standard
portable output (e.g., LT^-2).Dimension symbols of some quantities are specified to use Unicode
signs by the ISQ (e.g., Θ symbol for
the thermodynamic temperature dimension). The library follows
this by default. From the engineering point of view, sometimes Unicode
text might not be the best solution as terminals of many (especially
embedded) devices can output only letters from the basic literal
character set only. In such a case, the dimension symbol can be forced
to be printed using such characters thanks to character-set
token:
std::println("{}", isq::dim_thermodynamic_temperature); // Θ
std::println("{:P}", isq::dim_thermodynamic_temperature); // O
std::println("{}", isq::power.dimension); // L²MT⁻³
std::println("{:P}", isq::power.dimension); // L^2MT^-3unit-format-spec = [fill-and-align], [width], [unit-spec];
unit-spec = [character-set], [unit-symbol-solidus], [unit-symbol-separator], [L]
| [character-set], [unit-symbol-separator], [unit-symbol-solidus], [L]
| [unit-symbol-solidus], [character-set], [unit-symbol-separator], [L]
| [unit-symbol-solidus], [unit-symbol-separator], [character-set], [L]
| [unit-symbol-separator], [character-set], [unit-symbol-solidus], [L]
| [unit-symbol-separator], [unit-symbol-solidus], [character-set], [L];
unit-symbol-solidus = '1' | 'a' | 'n';
unit-symbol-separator = 's' | 'd';
In the above grammar:
fill-and-align
and width tokens are defined in the
28.5.2.2
[format.string.std]
chapter of the C++ standard specification,unit-symbol-solidus
token specifies how the division of units should look like:
/ only when
there is only one unit in the denominator, otherwise
negative exponents are printed (e.g.,
m/s,
kg m⁻¹ s⁻¹)m/s, kg/(m s))m s⁻¹,
kg m⁻¹ s⁻¹)unit-symbol-separator
token specifies how multiplied unit symbols should be separated:
kg m²/s²)⋅) as a separator (e.g.,
kg⋅m²/s²)
(requires the UTF-8 encoding)Note: The intent of the above grammar was that the elements of
unit-spec
can appear in any order as they have unique characters. Users shouldn’t
have to remember the order of those tokens to control the formatting of
a unit symbol.
The above grammar for unit-symbol-solidus
is consistent with the current state of [mp-units]. However, a few aternatives
are possible:
unit-symbol-solidus = '1' | 'a' | 'n';
unit-symbol-solidus = 'o' | 'a' | 'n';
unit-symbol-solidus = '1' | '*' | '0';
unit-symbol-solidus = '1' | '*' | '-';
unit-symbol-solidus = '1' | '+' | '-';
Unit symbols of some quantities are specified to use Unicode signs by
the [SI] (e.g.,
Ω symbol for the resistance
quantity). The library follows this by default. From the engineering
point of view, sometimes Unicode text might not be the best solution as
terminals of many (especially embedded) devices can output only letters
from the basic literal character set only. In such a case, the unit
symbol can be forced to be printed using such characters thanks to character-set
token:
std::println("{}", si::ohm); // Ω
std::println("{:P}", si::ohm); // ohm
std::println("{}", us); // µs
std::println("{:P}", us); // us
std::println("{}", m / s2); // m/s²
std::println("{:P}", m / s2); // m/s^2Additionally, both [ISO/IEC 80000] and [SI] leave some freedom on how to print unit symbols. This is why two additional tokens were introduced.
unit-symbol-solidus
specifies how the division of units should look like. By default,
/ will be
used only when the denominator contains only one unit. However, with the
‘a’ or ‘n’ options, we can force the facility to print the
/ character
always (even when there are more units in the denominator), or never, in
which case a parenthesis will be added to enclose all denominator
units.
std::println("{}", m / s); // m/s
std::println("{}", kg / m / s2); // kg m⁻¹ s⁻²
std::println("{:a}", m / s); // m/s
std::println("{:a}", kg / m / s2); // kg/(m s²)
std::println("{:n}", m / s); // m s⁻¹
std::println("{:n}", kg / m / s2); // kg m⁻¹ s⁻²The unit-symbol-separator
token allows us to obtain the following outputs:
std::println("{}", kg * m2 / s2); // kg m²/s²
std::println("{:d}", kg * m2 / s2); // kg⋅m²/s²Note: ‘d’ requires the UTF-8 encoding to be set.
quantity-format-spec = [fill-and-align], [width], [quantity-specs], [defaults-specs];
quantity-specs = conversion-spec;
| quantity-specs, conversion-spec;
| quantity-specs, literal-char;
literal-char = ? any character other than '{', '}', or '%' ?;
conversion-spec = '%', placement-type;
placement-type = subentity-id | '?' | '%';
defaults-specs = ':', default-spec-list;
default-spec-list = default-spec;
| default-spec-list, default-spec;
default-spec = subentity-id, '[' format-spec ']';
subentity-id = 'N' | 'U' | 'D';
format-spec = ? as specified by the formatter for the argument type ?;
In the above grammar:
fill-and-align
and width tokens are defined in the
28.5.2.2
[format.string.std]
chapter of the C++ standard specification,placement-type
token specifies which entity should be put and where:
space_before_unit_symbol for this
unit,defaults-specs
token allows overwriting defaults for the underlying formatters with the
custom format string. Each override starts with a subentity identifier
(‘N’, ‘U’, or ‘D’) followed by the format string enclosed in square
brackets.To format quantity values, the
formatting facility uses quantity-format-spec.
If left empty, the default formatting is applied. The same default
formatting is also applied to the output streams. This is why the
following code lines produce the same output:
std::cout << "Distance: " << 123 * km << "\n";
std::cout << std::format("Distance: {}\n", 123 * km);
std::cout << std::format("Distance: {:%N%?%U}\n", 123 * km);Please note that for some quantities the {:%N %U}
format may provide a different output than the default one, as some
units have space_before_unit_symbol
customization point explicitly set to
false (e.g.,
% and
°).
Thanks to the grammar provided above, the user can easily decide to either:
print a whole quantity:
std::println("Speed: {}", 120 * km / h);Speed: 120 km/hprovide custom quantity formatting:
std::println("Speed: {:%N in %U}", 120 * km / h);Speed: 120 in km/hprovide custom formatting for components:
std::println("Speed: {::N[.2f]U[n]}", 100. * km / (3 * h));Speed: 33.33 km h⁻¹print only specific components (numerical value, unit, or dimension):
std::println("Speed:\n- number: {0:%N}\n- unit: {0:%U}\n- dimension: {0:%D}", 120 * km / h);Speed:
- number: 120
- unit: km/h
- dimension: LT⁻¹placement-type
greatly simplify element access to the elements of the quantity. Without
them the second case above would require the following:
const quantity q = 120 * km / h;
std::println("Speed:\n- number: {}\n- unit: {}\n- dimension: {}",
q.numerical_value_ref_in(q.unit), q.unit, q.dimension);default-spec
is crutial to provide formatting of user-defined representation types.
Initially, [mp-units] library was providing
numerical value modifiers inplace of its format specification similarly
to std::chrono::duration
formatter. However, it:
The representation type used as a numerical value of a quantity must
provide its own formatter specialization. It will be called by the
quantity formatter with the format-spec provided by the user in the
N defaults specification.
In case we use C++ fundamental arithmetic types with our quantities the standard formatter specified in format.string.std will be used. The rest of this chapter assumes that it is the case and provides some usage examples.
sign token allows us to specify
how the value’s sign is being printed:
std::println("{0},{0::N[+]},{0::N[-]},{0::N[ ]}", 1 * m); // 1 m,+1 m,1 m, 1 m
std::println("{0},{0::N[+]},{0::N[-]},{0::N[ ]}", -1 * m); // -1 m,-1 m,-1 m,-1 mwhere:
+
indicates that a sign should be used for both non-negative and negative
numbers,-
indicates that a sign should be used for negative numbers and negative
zero only (this is the default behavior),<space>
indicates that a leading space should be used for non-negative numbers
other than negative zero, and a minus sign for negative numbers and
negative zero.precision token is allowed only
for floating-point representation types:
std::println("{::N[.0]}", 1.2345 * m); // 1 m
std::println("{::N[.1]}", 1.2345 * m); // 1 m
std::println("{::N[.2]}", 1.2345 * m); // 1.2 m
std::println("{::N[.3]}", 1.2345 * m); // 1.23 m
std::println("{::N[.0f]}", 1.2345 * m); // 1 m
std::println("{::N[.1f]}", 1.2345 * m); // 1.2 m
std::println("{::N[.2f]}", 1.2345 * m); // 1.23 mtype specifies how a value of the
representation type is being printed. For integral types:
std::println("{::N[b]}", 42 * m); // 101010 m
std::println("{::N[B]}", 42 * m); // 101010 m
std::println("{::N[d]}", 42 * m); // 42 m
std::println("{::N[o]}", 42 * m); // 52 m
std::println("{::N[x]}", 42 * m); // 2a m
std::println("{::N[X]}", 42 * m); // 2A mThe above can be printed in an alternate version thanks to the
# token:
std::println("{::N[#b]}", 42 * m); // 0b101010 m
std::println("{::N[#B]}", 42 * m); // 0B101010 m
std::println("{::N[#o]}", 42 * m); // 052 m
std::println("{::N[#x]}", 42 * m); // 0x2a m
std::println("{::N[#X]}", 42 * m); // 0X2A mFor floating-point values, the
type token works as follows:
std::println("{::N[a]}", 1.2345678 * m); // 1.3c0ca2a5b1d5dp+0 m
std::println("{::N[.3a]}", 1.2345678 * m); // 1.3c1p+0 m
std::println("{::N[A]}", 1.2345678 * m); // 1.3C0CA2A5B1D5DP+0 m
std::println("{::N[.3A]}", 1.2345678 * m); // 1.3C1P+0 m
std::println("{::N[e]}", 1.2345678 * m); // 1.234568e+00 m
std::println("{::N[.3e]}", 1.2345678 * m); // 1.235e+00 m
std::println("{::N[E]}", 1.2345678 * m); // 1.234568E+00 m
std::println("{::N[.3E]}", 1.2345678 * m); // 1.235E+00 m
std::println("{::N[g]}", 1.2345678 * m); // 1.23457 m
std::println("{::N[g]}", 1.2345678e8 * m); // 1.23457e+08 m
std::println("{::N[.3g]}", 1.2345678 * m); // 1.23 m
std::println("{::N[.3g]}", 1.2345678e8 * m); // 1.23e+08 m
std::println("{::N[G]}", 1.2345678 * m); // 1.23457 m
std::println("{::N[G]}", 1.2345678e8 * m); // 1.23457E+08 m
std::println("{::N[.3G]}", 1.2345678 * m); // 1.23 m
std::println("{::N[.3G]}", 1.2345678e8 * m); // 1.23E+08 mstd-format-specBoth [ISO/IEC 80000] and [SI] are recommending printing numbers into separated into groups of three:
To facilitate the reading of numbers with many digits, these may be separated into groups of three, counting from the decimal sign towards the left and the right. In the case where there is no decimal part (and thus no decimal marker), the counting shall be from the right-most digit, towards the left. No group shall contain more than three digits, except that when there are only four digits before or after the decimal marker it is customary not to use a space to isolate a single digit. Where such separation into groups of three is used, the groups shall be separated by a small space and not by a point or a comma or by any other means.
EXAMPLE 1: 12 345
EXAMPLE 2: 1 234 or 1234
EXAMPLE 3: 1 234,567 8 or 1234,5678The practice of grouping digits in this way is a matter of choice. It is not always followed in certain specialized applications such as engineering drawings and scripts to be read by a computer. The separation into groups of three should not be used for ordinal numbers used as reference numbers. A year, when given by four digits, shall always be written without a space between the digits.
As of today, no flag in std-format-spec
would force it. Similar output may be obtained thanks to localization,
but international standards mentioned above recommend that for every
user, no matter what localization option is being used.
std::chrono::durationThis library prints the quantities and their units according to
specific ISO specifications. Unfortunately, this is not the case for
std::chrono::duration:
using my_duration = std::chrono::duration<int, std::ratio<1, 4>>;
inline constexpr Unit auto my_unit = mag_ratio<1, 4> * si::second;
std::println("{}", std::chrono::seconds(42));
std::println("{}", 42 * s);
std::println("{}", my_duration(100));
std::println("{}", 100 * my_unit);The above prints:
42s
42 s
100[1/4]s
100 (1/4 s)
We are unsure if that is a problem that we should be worried about.
If so, we could consider adding ISO-compatible formatting to
std::chrono
abstractions, but it is not planned in the scope of this paper.
The library does not provide a text output for quantity points. The quantity stored inside is just an implementation detail of this type. It is a vector from a specific origin. Without the knowledge of the origin, the vector by itself is useless as we can’t determine which point it describes.
In the current library design, point origin does not provide any text in its definition. Even if we could add such information to the point’s definition, we would not know how to output it in the text. There may be many ways to do it. For example, should we prepend or append the origin part to the quantity text?
For example, the text output of
42 m for a
quantity point may mean many things. It may be an offset from the
mountain top, sea level, or maybe the center of Mars. Printing
42 m AMSL
for altitudes above mean sea level is a much better solution, but the
library does not have enough information to print it that way by
itself.
Should we somehow provide text support for quantity points? What about temperatures?
How to name symbol_text
accessor member functions (e.g., .portable())?
The same names should consistently be used in
character_set and in the formatting
grammar.
What about the localization for units? Will we get something like ICU in the C++ standard?
Do we care about ostreams enough to introduce custom manipulators to format dimensions and units?
std::chrono::duration
uses ‘Q’ and ‘q’ for a number and a unit. In the grammar above, we
proposed using ‘N’ and ‘U’ for them, respectively. We also introduced
‘D’ for dimensions. Are we OK with this?
Are we OK with the usage of
'_'
for denoting a subscript identifier? Should we use it everywhere
(consistency) or only where there is no dedicated Unicode subscript
character?
Are we OK with using Unicode characters for unit symbols in the code:
quantity resistance = 60 * kΩ;
quantity capacitance = 100 * µF;Can we have digits grouping for numbers?
After a rough introduction of most of the features and abstractions in the library, it might be good to discuss the scope for the Core Library Framework.
We have several significant features to consider here:
quantity, symbolic expressions,
dimensions, units, references, and concepts for them,quantity_point, point origins, and
concepts for them,quantity, units, and
dimensions,Please note that the above only lists the features present in this proposal. Additional features, like definitions of specific systems of quantities and units, math utilities, and other extensions, may be provided in the follow-up papers. We chose not to include those features here because they can be separately added later. This also means that we believe that all of the features listed above should be provided in the first release of the library.
To prove that, let’s try to identify possible problems if a specific feature is excluded from the MVP scope:
Core library
It just has to be there with all of the components listed. Otherwise, nothing works.
Quantity kinds
If we remove this feature, we would not be able to make a distinction
between Hz,
Bq, and
Bd, or
rad,
sr and
bit, or
Gy and
Sv as the quantities associated with
those units have the same dimensions. It is not only about units. It
also means that we will be able to pass a quantity of solid angular
measure to a function that takes
angular measure. Users will also not
be able to model their own distinct abstractions like we showed in the
case of the audio example (samples, beats, etc.). We also need to note
that we need that feature to be able to model the International System
of Quantities (ISQ). Skipping it is a serious usability and safety issue
that we should prevent.
Deciding to postpone this feature will block us from providing proper SI definitions, as the units of this system should be properly constrained for specific quantity kinds (possibly of the same dimension).
Various quantities of the same kind
Production feedback confirms this is a groundbreaking feature preventing critical bugs: warehouse robots misinterpreting box dimensions, flight computers passing forward velocity to sink rate parameters, or kinetic energy substituting for potential energy.
Beyond convertibility, hierarchies validate derived quantity
construction: kinetic energy cannot implicitly convert from
m g h (recipe for gravitational
potential energy). This validates correct ingredients in quantity
equations.
Without hierarchies, we cannot:
W vs
VA vs
var in AC circuits)Removing quantity hierarchies eliminates quantity-safety entirely, reducing the library to basic dimensional analysis. Postponing affects SI system modeling irreversibly—SI units provided without quantity specifications cannot be improved later. ISQ modeling becomes impossible.
The affine space
This looks like a feature that can be added later, and it is partially true. However, the lack of this feature will prevent us from modeling temperatures correctly, which means that we will have big problems defining SI units as the degree Celsius unit needs an offset to kelvin. If we postpone and release SI first, then we will not be able to improve the degree Celsius definition later on.
Skipping this feature also means that we will lack very important building block in modeling many problems in engineering. Those abstractions are considered so important that the BSI (British Standards Institution) already voted that they would strongly oppose a library not having this feature.
Also, without it, we will not be able to provide proper
std::chrono
compatibility.
Text output
Again, this looks like a purely additive feature, but if we never decide to standardize it, then all the symbols provided in unit and dimension definitions will be useless. If we do not intend to have text output, we should remove symbol text from the core framework class templates. This is why we should take that decision now.
Physical quantities and units libraries prevent errors at compile time through multiple safety layers. All safety features described here have zero runtime overhead—they’re enforced entirely at compile time, providing safety without performance cost.
This library provides six distinct safety levels:
Hz vs
Bq)All major C++ units libraries provide dimension safety (level 1) and unit safety (level 2). Some provide representation safety (level 3) and mathematical space safety (level 6). However, [mp-units] is the only C++ library implementing quantity kind safety (level 4) and quantity safety (level 5), making it uniquely comprehensive in its safety guarantees.
Dimension safety prevents mixing quantities with incompatible dimensions through automatic dimensional analysis:
quantity<si::metre / si::second> speed = 100 * km / h; // OK: km/h is speed (same as m/s)
quantity<si::second> time = 2 * h; // OK: hour is time (same as second)
quantity<si::metre> distance = speed * time; // OK: length
// quantity<si::metre> distance = 2 * h; // Error: incompatible dimensions!
// quantity<si::metre> distance = speed / time; // Error: wrong dimension!
// auto result = distance + time; // Error: cannot add length and time!All major C++ units libraries provide this foundational feature enabling dimensional analysis.
Unit safety ensures compatible units at interface boundaries (function arguments, return types, component integration).
// Function accepts any length and time units
quantity<si::metre / si::second> avg_speed(quantity<si::metre> d, quantity<si::second> t)
{
return d / t;
}
quantity distance = 220 * km;
quantity time = 2 * h;
quantity<km / h> speed = avg_speed(distance, time); // 110 km/h - automatic conversionUnit conversions are automated and checked at compile-time:
auto q1 = 5 * km;
std::cout << q1.in(m) << '\n'; // prints: 5000 m
quantity<si::metre, int> q2 = q1; // OK: km → mUnlike std::chrono::duration
which uses
std::ratio,
this library supports arbitrary conversion factors including irrational
numbers (π for radians/degrees) and extreme ratios (electronvolt: 1 eV =
1.602176634×10⁻¹⁹ J).
Legacy APIs often require raw numerical values. Always specify the unit explicitly:
void legacy_func(std::int64_t seconds);Bad (like std::chrono::duration::count()
- doesn’t specify unit):
struct X {
std::vector<std::chrono::milliseconds> vec;
};
X x;
x.vec.emplace_back(42s);
legacy_func(x.vec[0].count()); // Wrong if storage does not match seconds!Good (explicit unit specification):
struct X {
std::vector<quantity<si::milli<si::second>>> vec;
};
X x;
x.vec.emplace_back(42 * s);
legacy_func(x.vec[0].numerical_value_in(si::second)); // Safe
legacy_func(x.vec[0].force_numerical_value_in(si::second)); // If truncation OKThe member function numerical_value_ref_in(Unit)
enables direct access without conversion or copying:
void legacy_func(const int& joules);
quantity q1 = 42 * J;
quantity q2 = 42 * N * (2 * m);
quantity q3 = 42 * kJ;
legacy_func(q1.numerical_value_ref_in(si::joule)); // OK
legacy_func(q2.numerical_value_ref_in(si::joule)); // OK (equivalent unit)
legacy_func(q3.numerical_value_ref_in(si::joule)); // Compile-time error (different magnitude)
legacy_func((4 * J + 2 * J).numerical_value_ref_in(si::joule)); // Compile-time error (rvalue)This prevents most dangling references while acknowledging the value category ≠ lifetime limitation (see [Value Category Is Not Lifetime]).
Representation safety protects against numerical issues like overflow, underflow, and precision loss during conversions and arithmetic operations.
Conversions that would lose precision with integral types are prevented at compile-time:
quantity q1 = 5 * m;
std::cout << q1.in(km) << '\n'; // Compile-time error
quantity<si::kilo<si::metre>, int> q2 = q1; // Compile-time errorConverting 5 meters to kilometers with
int would
truncate to 0. To allow such conversions, use floating-point types or
explicit casts:
quantity q1 = 5. * m; // double representation
std::cout << q1.in(km) << '\n'; // OK: prints 0.005 kmquantity q1 = 5 * m; // int representation
std::cout << q1.in<double>(km) << '\n'; // OK: explicit conversion to double
std::cout << q1.force_in(km) << '\n'; // OK: explicit truncation (prints 0 km)
quantity<si::kilo<si::metre>, int> q2 = value_cast<km>(q1); // OK: explicit castThe same protection applies to representation type conversions:
quantity q1 = 2.5 * m;
quantity<si::metre, int> q2 = q1; // Compile-time error
quantity<si::metre, int> q3 = value_cast<int>(q1); // OK: explicit truncationCombined conversions (unit + representation) are supported to prevent intermediate overflow:
value_cast<Unit, Representation>(Quantity);
value_cast<Representation, Unit>(Quantity);
value_cast<Unit, Representation>(QuantityPoint);
value_cast<Representation, Unit>(QuantityPoint);
q.force_in<Representation>(Unit);
qp.force_in<Representation>(Unit);Converting small integral types between units can overflow even for non-zero values:
quantity q1 = std::int8_t(1) * km;
quantity q2 = q1.force_in(m); // Compile-time error (factor 1'000 > max int8_t)
if(q1 != 1 * m) { /* ... */ } // Compile-time errorThe conversion factor (1000) exceeds std::int8_t
range, so the library prevents the conversion even though 0 * km
would technically work. See Integer
overflow for details.
Note: No library can prevent runtime arithmetic overflow at
compile time (e.g., quantity * 2),
nor can they prevent floating-point overflow/underflow. For such cases,
use custom representation types with runtime checks.
explicit is
not explicit enoughConsider:
struct X {
std::vector<std::chrono::milliseconds> vec;
};
X x;
x.vec.emplace_back(42); // Compiles but fragile!If someone changes milliseconds
to microseconds, the code still
compiles but calculations are wrong by 1000×. The solution: require both
number and unit:
struct X {
std::vector<quantity<si::milli<si::second>>> vec;
};
X x;
x.vec.emplace_back(42); // Compile-time error
x.vec.emplace_back(42 * ms); // OKSimilarly, quantity_point
requires explicit origin association (unlike std::chrono::time_point):
quantity_point qp1 = mean_sea_level + 42 * m;
quantity_point qp2 = default_ac_temperature + 2 * delta<deg_C>;Quantity kind safety distinguishes between quantities sharing the same dimension but representing different physical concepts.
What should 1 * Hz + 1 * Bq + 1 * Bd
equal? Several leading libraries disagree:
[ISO/IEC Guide 99] states:
[ISO/IEC 80000] explicitly notes:
Measurement units of quantities of the same quantity dimension may be designated by the same name and symbol even when the quantities are not of the same kind. For example, joule per kelvin and J/K are respectively the name and symbol of both a measurement unit of heat capacity and a measurement unit of entropy, which are generally not considered to be quantities of the same kind. However, in some cases special measurement unit names are restricted to be used with quantities of specific kind only. For example, the measurement unit ‘second to the power minus one’ (1/s) is called hertz (Hz) when used for frequencies and becquerel (Bq) when used for activities of radionuclides.
[ISO/IEC 80000] explicitly states that frequency (Hz) and activity (Bq) are different kinds—they should not be comparable, added, or subtracted. Allowing such operations leads to safety issues when unrelated quantities of the same dimension are accidentally added or assigned:
quantity absorbed_dose = 1.5 * Gy;
quantity dose_equivalent = 2.0 * Sv;
// auto result = absorbed_dose + dose_equivalent; // Compile-time error!
// Error: cannot add absorbed dose and dose equivalent (both L²T⁻², but different kinds)
// QuantityOf<isq::absorbed_dose> auto d = 2.5 * Sv; // Compile-time error!
// Error: cannot initialize absorbed dose with dose equivalent[mp-units] is the only C++ library implementing quantity kind safety, fully distinguishing all SI quantity kinds including Gy/Sv, Hz/Bq, and rad/sr.
Quantity safety is the highest level, ensuring semantic correctness through:
Dimension-only libraries can’t distinguish between different quantities of the same kind:
class Box {
quantity<isq::area[m2]> base_;
quantity<isq::length[m]> height_; // Can't distinguish length, width, height!
public:
Box(quantity<isq::length[m]> l, quantity<isq::length[m]> w, quantity<isq::length[m]> h)
: base_(l * w), height_(h) {}
};[ISO/IEC 80000] defines hierarchies: width, height, radius are distinct but all are lengths. This library models these hierarchies to prevent errors:
// Quantity Type: Hierarchy prevents mixing energy types
void process_kinetic(quantity<isq::kinetic_energy[J]> ke) { /* ... */ }
quantity pe = isq::potential_energy(100 * J);
// process_kinetic(pe); // Compile-time error!
// Error: cannot pass potential_energy where kinetic_energy is required
// Quantity Type: Ingredient validation requires specific quantity types
quantity<isq::height[m]> h = 5 * m;
quantity<isq::gravitational_potential_energy[J]> Ep = mass * g * height;
// quantity<isq::gravitational_potential_energy[J]> wrong = mass * g * width; // Compile-time error!
// Error: cannot form gravitational potential energy from widthSee Systems of quantities for details.
While talking about quantities and units libraries, everyone expects that the library will protect (preferably at compile-time) from accidentally replacing multiplication with division operations or vice versa. Everyone knows and expects that the multiplication of length and time should not result in speed. It does not mean that such a quantity equation is invalid. It just results in a quantity of a different type.
If we expect the above protection for scalar quantities, we should also strive to provide similar guarantees for vector and tensor quantities. First, the multiplication or division of two vectors or tensors is not even mathematically defined. Such operations should be impossible on quantities using vector or tensor representation types.
What multiplication and division are for scalars, the dot and cross products are for vector quantities. The result of the first one is a scalar. The second one results in a vector perpendicular to both vectors passed as arguments. A good quantities and units library should protect the user from making such an error of accidentally replacing those operations.
Vector and tensor quantities can be implemented in two ways:
Encapsulating multiple quantities into a homogeneous vector or tensor representation type
This solution is the most common in the C++ market. It requires the quantities library to provide only basic arithmetic operations (addition, subtraction, multiplication, and division) which are being used to calculate the result of linear algebra math. However, this solution can’t provide any compile-time safety described above, and will also crash when someone passes a proper vector and tensor representation type to a quantity, expecting it to work.
Encapsulating a vector or tensor as a representation type of a quantity
This provides all the required type safety, but requires the library to implement more operations on quantities and properly constrain them so they are selectively enabled when needed. Besides [mp-units], the only library that supports such an approach is [Pint]. Such a solution requires the following operations to be exposed for quantity types (note that character refers to the algebraic structure of either scalar, vector and tensor):
a + b -
addition where both arguments should be of the same quantity kind and
charactera - b -
subtraction where both arguments should be of the same quantity kind and
charactera % b -
modulo where both arguments should be of the same quantity kind and
charactera * b -
multiplication where one of the arguments has to be a scalara / b -
division where the divisor has to be scalara ⋅ b - dot product of two
vectorsa × b - cross product of two
vectors|a|
- magnitude (norm) of a vector, i.e., norm(a)a ⊗ b - tensor product of two
vectors or tensorsa ⋅ b - inner product of two
tensorsa ⋅ b - inner product of tensor
and vectora : b -
scalar product of two tensorsAdditionally, the library knows the expected quantity character, which is provided (implicitly or explicitly) in the definition of each quantity type. Thanks to that, it prevents the user, for example, from providing a vector representation type for speed.
quantity q1 = isq::speed(60 * km / h); // OK
quantity q2 = isq::speed(la_vector{0, 0, -60} * km / h); // Compile-time error
quantity q3 = isq::velocity(60 * km / h); // OK
quantity q4 = isq::velocity(la_vector{0, 0, -60} * km / h); // OKAs we can see above, such features additionally improves the compile-time safety of the library by ensuring that quantities are created with proper quantity equations and are using correct representation types.
Complex quantities enforce domain-specific construction rules. Example: complex power requires active power and reactive power in correct order:
quantity<isq::complex_power[V * A], std::complex<double>> complex = get_power();
quantity<isq::active_power[W]> active = complex.real();
quantity<isq::reactive_power[var]> reactive = complex.imag();
quantity<isq::apparent_power[V * A]> apparent = complex.modulus();Mathematical space safety distinguishes between quantity
points (absolute positions) and quantity
vectors (differences/displacements).
quantity represents vectors;
quantity_point represents points.
This prevents nonsensical operations:
Forbidden operations:
home + airport
(what is “Boston + New York”?)distance - airport2 * airportAllowed operations:
airport - home →
distancehome + distance →
new location2 * distanceExample with temperature: Temperatures are points on a scale with an origin; temperature changes are vectors. You can add temperature changes, but adding two temperatures is meaningless:
// Points: Positions on a scale with an origin
quantity_point room_temp = point<deg_C>(20.);
quantity_point outside_temp = point<deg_C>(5.);
quantity temp_diff = room_temp - outside_temp; // OK: 15 K (vector)
// auto temp_sum = room_temp + outside_temp; // Compile-time error!
// Error: cannot add points (meaningless: what is 20 °C + 5 °C?)
// Vectors: Differences between values
quantity temp_change = delta<K>(10);
quantity_point new_temp = room_temp + temp_change; // OK: point + vector = 30 °C
quantity total_change = temp_change + temp_change; // OK: vector + vector = 20 K
// auto wrong = temp_change - room_temp; // Compile-time error!
// Error: cannot subtract point from vector (meaningless: what is 10 K - 20 °C?)Examples where mathematical space safety prevents errors: temperature (cannot add 20 °C + 10 °C, but can compute difference), time (cannot add two timestamps, but can subtract them), position (cannot add GPS coordinates, but can compute displacement), altitude (cannot add two elevations, but can compute height difference).
If we expect 120 * km / (2 * h)
to return 60 km / h,
we have to agree with the fact that 5 * km / (24 * h)
returns 0 km/h.
We can’t do a range check at runtime to dynamically adjust scales and
types based on the values of provided function arguments.
The same applies to:
static_assert(5 * h / (120 * min) == 0 * one);We may consider adding a special mode to detect the above cases at compile-time and try to bring the unit to a common unit before doing the operation. However, it will make it inconsistent with the following code:
static_assert(2 * m * (5 * h) / (120 * min) == 0 * m);If we decide to change the current behavior, it would:
km/s/Mpc).This is why floating-point representation types are recommended as a default to store the numerical value of a quantity. Some popular physical units libraries even forbid integer division at all.
The problem: Unit conversions multiply by hidden
factors. Comparing 11 * m > 12 * yd
converts both to a common unit (800 μm), multiplying by ~1000× under the
hood—easy to overflow small integer types.
Mitigation strategies: in fact, at the time of writing, new strategies are still being developed and tested. Here are the main strategies we have seen.
This is the simplest approach, and probably also the most popular: make the users responsible for avoiding overflow. The documentation may simply warn them to check their values ahead of time, as in this example from the bernedom/SI library. This valid approach places substantial responsibility on users, many unaware of the risk. Since unit conversions are hard to spot, this likely leads to the highest incidence of overflow bugs.
std::chrono
crafts user-facing types with generous ranges: all named durations
shorter than a day (hours to nanoseconds) represent ±292 years. Users
within this range who stick to these primary types avoid overflow.
This works well for time-only libraries but doesn’t scale to multi-dimensional units libraries where quantity types proliferate and users can create arbitrary combinations on the fly.
Overflow risk depends on: (1) conversion factor size (bigger = more risk)2, and (2) maximum representable value (larger = less risk).
An adaptive policy can forbid conversions where the “smallest
overflowing value” is “small enough to be scary”. [Au] uses threshold 2,147: if this value
converts without overflow, permit the operation. This prevents
operations failing on values under 1,000 while allowing common patterns
like 500 * mega<hertz>
in int32_t.
Production experience confirms this provides good default
protection.
This paper is more conservative: we fail conversion if the
representation can’t handle value
1 converted
to the destination unit (see Scaling overflow prevention),
pessimizing only the
0 case.
Runtime checks guarantee perfect safety. While unit conversions
rarely appear in hot loops, making runtime cost worthwhile, the main
challenge is error handling (exceptions,
optional,
expected, contracts, etc.).
A promising approach separates error detection and response: the library provides boolean checkers for overflow/truncation, then each project uses these with their preferred error handling mechanism.
Perhaps the most appealing approach to overflow in units libraries is
to delegate the problem to another library entirely. Quantity types can
work with any underlying numeric type (called the “rep”, as in the
chrono library) that satisfies
certain concepts related to basic arithmetic. If that rep comes from a
library that is dedicated to providing overflow-safe numeric types, then
the problem is solved without any additional effort on the units library
side.
This approach currently suffers from at least two significant downsides. First, it is less thoroughly tested in production usage, so we don’t know what the practical pitfalls are. Second, raw numeric types are likely to be overwhelmingly common in practice, and using this approach alone would leave this group of users unprotected—a group where less-experienced users are likely to be over-represented. Therefore, this can’t be the only solution to overflow.
Integers overflow on arithmetic (causing expensive failures [Ariane flight V88]) and truncate on
narrowing assignment. Floating-point types lose precision on narrowing,
and int64_t
to double
conversion also loses precision.
Safe numeric types in the standard library would address these
concerns as quantity reps. A type
trait indicating value-preserving conversions would also help.
Units compose to create derived units (constexpr Unit auto kmph = km / h;),
an industry standard in [Boost.Units] and [Pint].
However, order of operations can surprise users:
quantity q = 60 * km / 2 * h; // Results in 30 km⋅h, not 30 km/h
quantity q = 60 * km / (2 * h); // Requires parentheses for 30 km/hGeneric code can also produce unexpected types:
template<typename T>
auto make_length(T v) { return v * si::metre; }
quantity v = 42 * m;
quantity q = make_length(v); // Returns area (m²), not length![mp-units] initially disallowed
multiplying/dividing quantities by units to prevent this, but requiring
60 * (km / h)
proved too verbose and confusing.
These issues always surface as compile-time errors when assigning to explicitly-typed quantities:
quantity<si::kilo<si::metre> / non_si::hour, int> q1 = 60 * km / 2 * h; // Error
QuantityOf<isq::speed> auto q3 = 60 * km / 2 * h; // Error
quantity<si::metre, int> q1 = make_length(42 * m); // Error
QuantityOf<isq::length> auto make_length(T v) { return v * si::metre; } // Constrains return typeModeling systems of quantities improves safety but has pitfalls in corner cases.
While length * length → area
makes sense bidirectionally, width * height → area
is unidirectional—not all areas are width×height products:
static_assert(implicitly_convertible(isq::width * isq::height, isq::area));
static_assert(!implicitly_convertible(isq::area, isq::width * isq::height));Surprisingly, height * height → area
behaves similarly. While hard to imagine physically, the library cannot
prevent such operations.
Dividing quantities of the same kind yields dimension-one quantities with different meanings (slope of ramp, clock accuracy), yet they’re mutually comparable per dimensional analysis.
The above means that the following code is valid:
quantity q1 = isq::length(1. * m) / isq::length(10. * m) + isq::time(1. * us) / isq::time(1 * h);
quantity q2 = isq::height(1. * m) / isq::length(10. * m) + isq::time(1. * us) / isq::time(1 * h);Both produce dimensionless (root
of hierarchy). Converting q2 and
q3 to specific dimension-one
quantities works for general forms but requires explicit conversion for
specific combinations:
quantity<(isq::length / isq::length)[m / m]> ok1 = q1; // OK (same quantity)
quantity<(isq::length / isq::length)[m / m]> ok2 = q2; // OK (same quantity)
quantity<(isq::height / isq::length)[m / m]> bad1 = q1; // Error (not every dimensionless is height/length)
quantity<(isq::height / isq::length)[m / m]> bad2 = q2; // Error (not every dimensionless is height/length)The quantity and
quantity_point class templates are
structural types to allow them to be passed as template arguments. For
example, we can write the following:
constexpr struct amsterdam_sea_level : absolute_point_origin<isq::altitude> {
} amsterdam_sea_level;
constexpr struct mediterranean_sea_level : relative_point_origin<amsterdam_sea_level + isq::altitude(-27 * cm)> {
} mediterranean_sea_level;
using altitude_DE = quantity_point<isq::altitude[m], amsterdam_sea_level>;
using altitude_CH = quantity_point<isq::altitude[m], mediterranean_sea_level>;Unfortunately, current language rules require that all member data of a structural type are public. This could be considered a safety issue. We try really hard to provide unit-safe interfaces, but at the same time expose the public “naked” data member that can be freely read or manipulated by anyone.
Hopefully, this requirement on structural types will be relaxed before the library gets standardized.
Note: This chapter provides more design details and rationale for them. It tries to not repeat information already provided in the previous chapters so a reader is expected to be familar with them already.
Before we dig into details, it is worth reminding that compile-time errors generation is the most important feature of the library. If we did not make errors in our code and could handle quantities and write all the conversions correctly by hand, such a library would be of little use. We are humans and make mistakes.
Also, the library is about to be used by many engineers who use C++ as a tool to get their work done and are not C++ template metaprogramming experts. This is why the compilation errors generated by this library should be as easy to understand as possible. Users should be able to quickly identify the cause of the issue and understand how to fix it.
With the above in mind, the [mp-units] library decided to use a rather unusual pattern to define entities, but it proved really successful, and we have received great feedback from users.
To improve the readability of compiler errors and types presented in a debugger, and to make it easier to correlate them with a user’s written code, a new idiom in the library is to use the same identifier for a tag type and its instance.
Here is how we define metre and
second [SI] base units:
inline constexpr struct metre : named_unit<"m", kind_of<isq::length>> {} metre;
inline constexpr struct second : named_unit<"s", kind_of<isq::time>> {} second;Please note that the above reuses the same identifier for a type and its value. The rationale behind this is that:
Ordinary users don’t care about what is a type and what is a value in the error message. They want to be able to easily read and analyze the error message and understand where in the code they made the calculation error.
Unfortunately, we can’t be consistent here. The C++ language rules do
not allow to use the same identifier for a template and the object
resulting from its instantiation. For such cases, we decided to postfix
the template identifier with
'_'.
Let’s compare the readability of the current practices with an
alternative and popular usage of _t
postfixes for type identifiers (after removing the project namespace
prefix):
Current practice:
User’s code
|
Resulting type
|
|---|---|
quantity<si::metre> |
quantity<si::metre{}, double> |
quantity<si::metre / si::second> |
quantity<derived_unit<si::metre, per<si::second>>{}, double> |
isq::speed(50 * km / h) / (5 * s) |
quantity<reference<derived_quantity_spec<isq::speed, per<isq::time>>, derived_unit<si::kilo_<si::metre>, per<non_si::hour, si::second>>>{}, int> |
With _t postfixes:
User’s code
|
Resulting type
|
|---|---|
quantity<si::metre> |
quantity<si::metre_t{}, double> |
quantity<si::metre / si::second> |
quantity<derived_unit<si::metre_t, per<si::second_t>>{}, double> |
isq::speed(50 * km / h) / (5 * s) |
quantity<reference<derived_quantity_spec<isq::speed_t, per<isq::time_t>>, derived_unit<si::kilo_t<si::metre_t>, per<non_si::hour_t, si::second_t>>>{}, int> |
To improve the types readability we also prefer to use type identifiers for template parameters (if possible) rather than NTTPs directly. Without it, the last type would look as follows:
User’s code
|
Resulting type
|
|---|---|
isq::speed(50 * km / h) / (5 * s) |
quantity<reference<derived_quantity_spec<isq::speed_t{}, per<isq::time_t{}>>{}, derived_unit<si::kilo_t<si::metre_t{}>{}, per<non_si::hour_t{}, si::second_t{}>>{}>{}, int> |
Moreover, to prevent possible issues in the library’s framework
compile-time logic, all of the library’s entities must be marked
final. This
prevents the users from deriving their own strong types from them, which
would prevent symbolic expressions simplification of equivalent
entities. This constraint is enforced by the concepts in the
library.
Let’s look again at the above units definitions. Another essential point to notice is that all the types describing entities in the library are short, nicely named identifiers that derive from longer, more verbose class template instantiations. This is really important to improve the user experience while debugging the program or analyzing the compilation error.
Note: Such a practice is rare in the industry. Some popular C++ physical units libraries generate enormously long error messages.
Many physical units libraries (in C++ or any other programming
language) assign strong types to library entities (e.g., derived units).
While metre_per_second as a type may
not look too scary, consider, for example, units of angular momentum. If
we followed this path, its coherent unit would look like
kilogram_metre_sq_per_second. Now,
consider how many scaled versions of this unit you would predefine in
the library to ensure that all users are happy with your choice? How
expensive would it be from the implementation point of view? How
expensive would it be to standardize?
This is why, in this library, we put a strong requirement to make everything as composable as possible. For example, to create a quantity with a unit of speed, one may write:
quantity<si::metre / si::second> q;In case we use such a unit often and would prefer to have a handy helper for it, we can always do something like this:
constexpr auto metre_per_second = si::metre / si::second;
quantity<metre_per_second> q;or choose any shorter identifier of our choice.
The unit composition works not only on the “unit-level”. We can multiply or divide a quantity to get another type of quantity expressed in a composed unit:
quantity pace = (4. * min + 40. * s) / km;Coming back to the angular momentum case, thanks to the composability of units, a user can create such a quantity in the following way:
using namespace si::unit_symbols;
auto q = la_vector{1, 2, 3} * isq::angular_momentum[kg * m2 / s];It is a much better solution. It is terse and easy to understand.
Please also notice how easy it is to obtain any scaled version of such a
unit (e.g., mg * square(mm) / min)
without having to introduce hundreds of types to predefine them.
This library is based on C++20, significantly improving user experience. One such improvement is the usage of value-based equations.
As we have learned above, the entities are being used as values in
the code, and they compose. Moreover, derived entities can be defined in
the library using such value-based equations. This is a considerable
improvement compared to what we can find in other physical units
libraries or what we have to deal with when we want to write some
equations for
std::ratio.
For example, below are a few definitions of the [SI] derived units showing the power of C++20 extensions to Non-Type Template Parameters, which allow us to directly pass a result of the value-based unit equation to a class template definition:
inline constexpr struct newton : named_unit<"N", kilogram * metre / square(second)> {} newton;
inline constexpr struct pascal : named_unit<"Pa", newton / square(metre)> {} pascal;
inline constexpr struct joule : named_unit<"J", newton * metre> {} joule;Several proposed class templates like:
derived_unit,
derived_dimension,
derived_quantity_spec,per,
power,should not be explicitly instantiated by the user.
Those types are the results of running operators on objects and have strict requirements on how the template arguments are provided. The library instantiates such class templates in a particular way (i.e., the arguments must be provided in the correct order). This logic might be partially implementation-defined (e.g., the order of elements in a numerator or denominator is sorted by type-id (whatever it means)). The important part here is to keep the ordering rules consistent within a specific implementation. Otherwise, the library cannot do its job properly (e.g., units simplification will not work).
Here are some examples:
constexpr auto u1 = kg * m / s2;
constexpr auto u2 = kg * (m / s2);
constexpr auto u3 = kg / s2 * m;All of the above yield the same instantiation of the derived_unit<si::kilo_<si::gram>, si::metre, per<power<si::second, 2>>>.
The user never needs to instantiate the class templates explicitly. Allowing this can cause problems, as the user can make ordering errors. Of course, we can constrain the class template to require arguments in a particular order, but it will only slow down the compile times for every instantiation by the library’s engine.
Please note that a user in this library always works with values of
tag types (e.g., unit), but the
derived_unit class template takes
those tag types as type parameters. This yields more readable types and
prevents users from instantiating the template by themselves.
Here is how units are defined:
inline constexpr struct second : named_unit<"s", kind_of<isq::time>> {} second;
inline constexpr struct metre : named_unit<"m", kind_of<isq::length>> {} metre;
inline constexpr struct gram : named_unit<"g", kind_of<isq::mass>> {} gram;
inline constexpr auto m = metre;
inline constexpr auto s = second;
inline constexpr auto g = gram;
inline constexpr auto s2 = square(second);Because of the above, to explicitly instantiate a class template, a user would need to type the following:
derived_unit<si::kilo_<struct si::gram>, per<power<struct si::second, 2>>> u4;The usage of the explicit
struct
keyword might be surprising to many users.
Also, when dealing with quantities, a user does not need to spell a unit’s type:
void foo(quantity<si::kilo<si::gram> * si::metre / square(si::second)>) {}
foo(42. * kg * m / s2);The above instantiates a quantity<derived_unit<si::kilo_<si::gram>, si::metre, per<power<si::second, 2>>>{}, double>
behind the scenes.
Please note that in the above examples, all the class templates
derived_unit,
per,
power, and
kilo_ have the same property. They
are well-defined identifiers that users expect to see in types presented
in the debugger or compilation errors. This is why they should not be
exposition-only and be regular members of the
std namespace. Every implementation
has to spell and use them in the same way.
[mp-units] library decided to define
those class templates in the
mp_units namespace, but to not
export them from the mp_units.core
module. With this, users will have no way to instantiate those templates
by themselves, which is the exact intent of this library. Initially, we
wanted to propose something similar for the Standard Library, but the LWG did not
like the idea.
An alternative might be to state that it is IFNDR or UB to instantiate those by the user. It does not technically prevent users from doing so, but if they do it, they are on their own.
Last but not least, we can allow such instantiations and add restrictive constraints to verify template arguments, which could affect compilation times.
The below graph presents the most important entities of the library’s framework and how they relate to each other.
Some of the entities were already introduced in the Quick domain introduction chapter. Below we describe the remaining ones.
[ISO/IEC 80000] explicitly states that quantities (even of the same kind) may have different characters:
The quantity character in the library is implemented with the
quantity_character enumeration:
enum class quantity_character { real_scalar, complex_scalar, vector, tensor };More information on quantity characters can be found in the Safe operations of vector and tensor quantities chapter.
Dimension is not enough to describe a quantity. This is why [ISO/IEC 80000] provides hundreds of named quantity types. It turns out that there are many more quantity types in the ISQ than the named units in the [SI].
This is why the library introduces a quantity specification entity that stores:
For example:
Quantity Specification
|
Dimension
|
Quantity Kind
|
Character
|
Equation
|
|---|---|---|---|---|
isq::duration |
T | isq::duration |
scalar | base quantity |
isq::length |
L | isq::length |
scalar | base quantity |
isq::mass |
M | isq::mass |
scalar | base quantity |
isq::width |
L | isq::length |
scalar | isq::length |
isq::height |
L | isq::length |
scalar | isq::length |
isq::position_vector |
L | isq::length |
vector | isq::length |
isq::displacement |
L | isq::length |
vector | isq::length |
isq::area |
L² | isq::area |
scalar | isq::length² |
isq::speed |
LT⁻¹ | isq::speed |
scalar | isq::length / isq::duration |
isq::velocity |
LT⁻¹ | isq::speed |
vector | isq::displacement / isq::duration |
isq::acceleration |
LT⁻² | isq::acceleration |
vector | isq::velocity / isq::duration |
isq::force |
LMT⁻² | isq::force |
vector | isq::mass * isq::acceleration |
isq::moment_of_force |
L²MT⁻² | isq::moment_of_force |
vector | isq::position_vector * isq::force |
A unit is a concrete amount of a quantity that allows us to measure the values of quantities of the same kind and represent the result as a number being the ratio of the two quantities.
For example:
si::second,
si::metre,
si::kilogram,
si::ampere,
si::kelvin,
si::mole,
and
si::candela
are the base units of the [SI].si::kilo<si::metre>
is a prefixed unit of length.si::radian,
si::newton,
and si::watt
are examples of named derived units within the [SI].non_si::minute is
an example of a scaled unit of time.si::si2019::speed_of_light_in_vacuum
is a physical constant standardized by the SI in 2019.Note: In this library, physical constants are also implemented as units.
Quantity representation defines the type used to store the numerical value of a quantity. Such a type should be of a specific quantity character provided in the quantity specification.
Note: By default, all floating-point and integral (besides
bool) types
are treated as real scalars and
std::complex
as a complex scalar.
In the affine space theory, the point origin specifies where the “zero” of our measurement’s scale is.
In this library, we have two types of point origins:
Note: More information on this subject can be found in The affine space chapter.
Quantity point implements a point in the affine space theory. Its value can be easily created by adding/subtracting the quantity with a point origin.
Note: More information on this subject can be found in The affine space chapter.
The library’s names fall into two distinct categories with different placement requirements.
std::The framework types (quantity,
quantity_point,
quantity_spec,
named_unit,
dimension,
mag, concepts, …) are a small, fixed
set. They provide long-awaited strong-type support not only for physical
quantities and units but also for anything that resembles a measurable
number (e.g., longitude and latitude, pixel
coordinates, prices). The affine space abstraction has
similarly broad applicability.
We propose placing all framework entities directly in namespace
std. The primary motivation is
error message quality: when a type mismatch occurs, the
compiler reports the full qualified name of every type involved. Nesting
framework types inside
std::units
or a similar intermediate namespace adds that prefix to every diagnostic
line, making already-complex template error messages significantly
harder to read. Since the library’s chief value proposition is catching
unit errors at compile time, readable diagnostics are a first-class
concern — arguably more so than for any other library.
Some entities would need to be renamed to avoid ambiguity with
existing std identifiers (e.g.,
reference).
During Croydon 2026 (SG18), three concerns were raised:
Coherence with the rest of the standard library.
Short names aid error messages within the library but may feel
inconsistent alongside the rest of
std::. We
acknowledge this tension; however, the alternative of longer qualified
names measurably harms the user experience that motivates the library’s
existence. Implementations also have scope to improve diagnostics beyond
what the library alone can do.
Volume of names in
std::.
The framework itself contributes a bounded, modest number of names.
System definitions (SI units, ISQ quantities) live in their own
subnamespaces and do not pollute
std::
directly.
Naming conflicts with existing identifiers. The
most concrete example raised was pi:
std::numbers::pi
is an existing floating-point constant, while this library defines
pi as a unit (a
dimensionless scaling factor). These are different things with different
types, so they cannot be confused by the compiler, but they are
conceptually distinct and having two entities called
pi in closely related namespaces may
surprise users. This is an open question — see [Dimensionless and
framework-level units] below.
Quantity types and units from concrete systems (SI, ISQ, US
customary, …) are placed in their own subnamespaces under
std:
using namespace std::si::unit_symbols;
std::quantity<std::si::metre> q = 42 * m;This keeps the system-specific vocabulary scoped and avoids polluting
std:: with
an unbounded set of unit and quantity names.
Beyond the framework types and the named systems, there are several entities that do not naturally belong to any concrete system namespace yet are more than just library machinery. They fall into roughly three categories with potentially different placement requirements.
dimension_one,
dimensionless, and
one are the algebraic identity
elements of the framework’s type system — the dimension, quantity
specification, and unit of dimensionless quantities respectively. They
are mandatory infrastructure: any quantity equation that produces a
dimensionless result relies on them implicitly. As such they are
arguably framework entities and could live in
std::
alongside quantity and
named_unit. However, placing
one directly in
std:: raises
a naming concern: to be consistent with the existing
dimension_one and
dimensionless names in the same
group, and to avoid potential conflicts with user-defined
one identifiers, it may need to be
renamed to unit_one.
pi — a system-required constantpi occupies a special position:
it is a mandatory magnitude factor for defining SI units. The SI
degree (plane angle) is defined as
mag<pi> / mag<180> * rad,
making pi a dependency of the SI
system itself, not merely a convenience for user code. Placing it in
std::si::
is therefore tempting, but wrong: users working with angles in non-SI
contexts (custom unit systems, pure mathematics, signal processing) need
pi independently of the SI system,
so requiring std::si::pi
would impose an unwanted SI dependency.
Placing it in
std::
alongside the framework types is the most accessible option. The
proximity to std::numbers::pi
might initially appear problematic — both represent π but are
fundamentally different kinds of entity: std::numbers::pi
is a floating-point approximation, while
std::pi
would be a compile-time exact symbolic constant, evaluated to
floating-point only when applied to a concrete value. However, this
paper argues throughout that
quantity is not merely a physics
type but a safer numeric wrapper for any arithmetic value. Under that
framing, pi can be seen as a safer
version of std::numbers::pi:
it carries the same mathematical meaning but defers floating-point
evaluation and composes exactly with unit magnitudes. The naming
proximity in
std:: would
then reflect a genuine relationship rather than a collision — especially
since the library uses std::numbers::pi
under the hood when materialising the floating-point value of
pi. Whether this reasoning is
sufficient to justify
std::pi is
ultimately a question for LEWG.
percent
(%),
per_mille
(‰), and
parts_per_million
(ppm) are widely used in production
code but, to our knowledge, are not defined by any standard system of
units (neither SI nor any other system included in this proposal). They
are convenient extensions of one
that belong to no particular domain. Whether they should share a
namespace with the algebraic identities or be grouped separately is
unclear.
per<U>
(reciprocal unit), delta<U>()
and point<U>()
(affine space constructors) are intentionally terse to keep quantity
equations and construction syntax readable. Their brevity could be
problematic in the flat
std::
namespace. However, unlike the units and quantity specs above, these are
helpers that form part of the core API and are arguably framework
entities in the same sense as
quantity itself.
The placement of all of the above is an open question for
LEWG. The three or four categories above may warrant different
namespaces, or a single shared subnamespace (e.g., std::units::
or std::qty::).
The tradeoff is consistent with the broader framework discussion: a
subnamespace reduces name-conflict risk but requires using namespace
to keep equations readable.
This chapter enumerates all the user-facing concepts in the library.
Note: Initially, C++20 was meant to use
CamelCase for all the concept
identifiers. 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. As long as
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 or
QuantitySpec. 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 concept identifier. However, we are aware that this might be a
temporary solution. In case the library gets standardized, we can expect
the LEWG to bikeshed/rename all of the concept identifiers to a
standard_case, even if it will
result in a harder to understand code. We propose some suggestions at
the end of the chapter.
Dimension<T> conceptDimension concept matches a
dimension of either a base or derived quantity:
base_dimension class template. It
should be instantiated with a unique symbol identifier describing this
dimension in a specific system of quantities.All of the above dimensions have to be marked as
final.
DimensionOf<T, V>
conceptDimensionOf concept is satisfied
when both arguments satisfy a Dimension
concept and when they compare equal.
QuantitySpec<T> conceptQuantitySpec concept matches all
the quantity specifications including:
quantity_spec class template
instantiated with a base dimension argument.quantity_spec class template
instantiated with a result of a quantity equation passed as an
argument.quantity_spec class template
instantiated with another “parent” quantity specification passed as an
argument.All of the above quantity specifications have to be marked as
final.
QuantitySpecOf<T, V>
conceptQuantitySpecOf concept is
satisfied when both arguments satisfy a QuantitySpec
concept and when T is implicitly
convertible to V.
UnitMagnitude<T>UnitMagnitude concept is
satisfied by all types defining a unit magnitude.
Note: Unit magnitude implementation is a private implementation detail of the library.
Unit<T>
conceptUnit concept matches all the
units in the library including:
named_unit class template
instantiated with a unique symbol identifier describing this unit in a
specific system of units.named_unit class template
instantiated with a unique symbol identifier and a product of
multiplying another unit with some magnitude.prefixed_unit class template
instantiated with a prefix symbol, a magnitude, and a unit to be
prefixed.named_unit class template
instantiated with a unique symbol identifier and a result of unit
equation passed as an argument.named_constant class template
instantiated with a unique symbol identifier and a product of
multiplying another unit with some magnitude.All of the above units have to be marked as
final.
PrefixableUnit<T>PrefixableUnit concept is
satisfied by all units derived from a
named_unit class template. Such
units can be passed as an argument to a
prefixed_unit class template.
UnitOf<T, V>
conceptUnitOf concept is satisfied for
all units T for which an associated
quantity spec is implicitly convertible to the provided
QuantitySpec value..
Reference<T>
conceptReference concept is satisfied by
all quantity reference types. Such types provide all the
meta-information required to create a Quantity.
A Reference can either be:
Unit.reference
class template with a QuantitySpec
passed as the first template argument and a Unit passed
as the second one.ReferenceOf<T, V>
conceptReferenceOf concept is satisfied
by references T which have a
quantity specification that satisfies QuantitySpecOf<V>
concept.
RepresentationOf<T, V>RepresentationOf concept
constrains a type T of a number that
stores the numerical value of a quantity.
Every representation type must satisfy a common baseline:
MagnitudeScalable:
the library must be able to apply a unit magnitude ratio to it
internally. Most standard types satisfy this automatically; see Representation Types for details.real()/imag()/modulus()
CPOs for complex scalars,
norm()/magnitude()
CPO for vectors).The second template argument V
further constrains which characters are accepted:
if the type of V satisfies QuantitySpec:
V describes a quantity kind,V.if V is of
quantity_character type:
Quantity<T>
conceptQuantity concept matches every
quantity in the library and is satisfied by all types being or deriving
from an instantiation of a quantity
class template.
QuantityOf<T, V>
conceptQuantityOf concept is satisfied
by all the quantities for which a ReferenceOf<V>
is true.
QuantityLike<T>
conceptQuantityLike concept provides
interoperability with other libraries and is satisfied by a type
T for which an instantiation of
quantity_like_traits type trait
yields a valid type that provides:
reference static data member
that matches the Reference
concept,rep type that matches RepresentationOf
concept with the character provided in
reference.explicit_import static data
member convertible to
bool that
specifies that the conversion from T
to a quantity type should happen
explicitly (if
true),explicit_export static data
member convertible to
bool that
specifies that the conversion from a
quantity type to
T should happen explicitly (if
true),to_numerical_value(T)
static member function returning a raw value of the quantity,from_numerical_value(rep)
static member function returning
T.For example, this is how support for std::chrono::seconds
can be provided:
template<>
struct quantity_like_traits<std::chrono::seconds> {
static constexpr auto reference = detail::time_unit_from_chrono_period<Period>();
static constexpr bool explicit_import = false;
static constexpr bool explicit_export = false;
using rep = Rep;
using T = std::chrono::duration<Rep, Period>;
[[nodiscard]] static constexpr rep to_numerical_value(const T& q) noexcept(
std::is_nothrow_copy_constructible_v<rep>)
{
return q.count();
}
[[nodiscard]] static constexpr T from_numerical_value(const rep& v) noexcept(
std::is_nothrow_copy_constructible_v<rep>)
{
return T(v);
}
};
quantity q = 42s;
std::chrono::seconds dur = 42 * s;PointOrigin<T>
conceptPointOrigin concept matches all
quantity point origins in the library. It is satisfied by either:
absolute_point_origin class
template.relative_point_origin class
template.PointOriginFor<T, V>
conceptPointOriginFor concept is
satisfied by all PointOrigin
types that have quantity type implicitly convertible from quantity
specification V, which means that
V must satisfy QuantitySpecOf<T::quantity_spec>.
For example, si::ice_point can
serve as a point origin for points of isq::Celsius_temperature
because this quantity type implicitly converts to isq::thermodynamic_temperature.
However, if we define
mean_sea_level in the following
way:
inline constexpr struct mean_sea_level : absolute_point_origin<isq::altitude> {} mean_sea_level;then it can’t be used as a point origin for points of
isq::length
or
isq::width
as none of them is implicitly convertible to isq::altitude:
QuantityPoint<T>
conceptQuantityPoint concept is
satisfied by all types being either a specialization or derived from
quantity_point class template.
QuantityPointOf<T, V>
conceptQuantityPointOf concept is
satisfied by all the quantity points
T that match the following value
V:
V
|
Condition
|
|---|---|
QuantitySpec |
The quantity point quantity specification satisfies ReferenceOf<V>
concept. |
PointOrigin |
The point and V have
the same absolute point origin. |
QuantityPointLike<T>
conceptQuantityPointLike concept
provides interoperability with other libraries and is satisfied by a
type T for which an instantiation of
quantity_point_like_traits type
trait yields a valid type that provides:
reference static data member
that matches the Reference
concept.point_origin static data member
that matches the PointOrigin
concept.rep type that matches RepresentationOf
concept with the character provided in
reference.explicit_import static data
member convertible to
bool that
specifies that the conversion from T
to a quantity_point type should
happen explicitly (if
true),explicit_export static data
member convertible to
bool that
specifies that the conversion from a
quantity_point type to
T should happen explicitly (if
true),to_numerical_value(T)
static member function returning a raw value of the quantity being the
offset of the point from the origin,from_numerical_value(rep)
static member function returning
T.For example, this is how support for a std::chrono::time_point
of std::chrono::seconds
can be provided:
template<typename C>
struct quantity_point_like_traits<std::chrono::time_point<C, std::chrono::seconds>> {
static constexpr auto reference = detail::time_unit_from_chrono_period<Period>();
static constexpr auto point_origin = chrono_point_origin<C>;
static constexpr bool explicit_import = false;
static constexpr bool explicit_export = false;
using rep = Rep;
using T = std::chrono::time_point<C, std::chrono::duration<Rep, Period>>;
[[nodiscard]] static constexpr rep to_numerical_value(const T& tp) noexcept(std::is_nothrow_copy_constructible_v<rep>)
{
return tp.time_since_epoch().count();
}
[[nodiscard]] static constexpr T from_numerical_value(const rep& v) noexcept(
std::is_nothrow_copy_constructible_v<rep>)
{
return T(std::chrono::duration<Rep, Period>(v));
}
};
quantity_point qp = time_point_cast<std::chrono::seconds>(std::chrono::system_clock::now());
std::chrono::sys_seconds q = qp + 42 * s;This chapter provides some alternative names in
standard_case for concepts.
Before
|
After
|
Alternative
|
Comments
|
|---|---|---|---|
Dimension |
dimension |
some_dimension |
|
DimensionOf |
dimension_of |
This concept is never used in the framework but might be useful to users. | |
QuantitySpec |
quantity_spec |
some_quantity_spec |
“After” requires renaming
quantity_spec to
named_quantity_spec for a class
template. |
QuantitySpecOf |
quantity_spec_of |
||
UnitMagnitude |
unit_magnitude |
some_unit_magnitude |
|
Unit |
unit |
some_unit |
“Alternative” allows renaming named_unit class template to unit |
PrefixableUnit |
prefixable_unit |
||
UnitOf |
unit_of |
||
Reference |
reference |
some_reference |
Collides with reference class
template, but we have some ideas how to remove the class template. |
ReferenceOf |
reference_of |
||
RepresentationOf |
representation_of |
||
Quantity |
delta_quantity |
some_quantity |
Collides with quantity class
template. |
QuantityOf |
delta_quantity_of |
quantity_of |
|
QuantityLike |
delta_quantity_like |
quantity_like |
|
PointOrigin |
point_origin |
some_point_origin |
|
PointOriginFor |
point_origin_for |
||
QuantityPoint |
point_quantity |
some_quantity_point |
Collides with quantity_point
class template. |
QuantityPointOf |
point_quantity_of |
quantity_point_of |
|
QuantityPointLike |
point_quantity_like |
quantity_point_like |
How do we like some_XXX practice?
It is already being used in some open source projects. It also reads
nicely against XXX_of, which
provides more restrictive constraints. If we are OK with it, should we
apply it only in the conflicting cases or apply everywhere for
consistency?
If we go for some_XXX then we can
leave quantity_spec as the class
template, and also we could rename
named_unit class template to
unit so the user can type less while
defining their own system entities.
Here is a comparison of quantity specification and units definitions in both alternatives:
Today (inconsistent?):
inline constexpr struct length : quantity_spec<dim_length> {} length;
inline constexpr struct time : quantity_spec<dim_time> {} time;
inline constexpr struct speed : quantity_spec<length / time> {} speed;
inline constexpr struct metre : named_unit<"m", kind_of<isq::length>> {} metre;
inline constexpr struct second : named_unit<"s", kind_of<isq::time>> {} second;Option 1:
inline constexpr struct length : named_quantity_spec<dim_length> {} length;
inline constexpr struct time : named_quantity_spec<dim_time> {} time;
inline constexpr struct speed : named_quantity_spec<length / time> {} speed;
inline constexpr struct metre : named_unit<"m", kind_of<isq::length>> {} metre;
inline constexpr struct second : named_unit<"s", kind_of<isq::time>> {} second;Option 2:
inline constexpr struct length : quantity_spec<dim_length> {} length;
inline constexpr struct time : quantity_spec<dim_time> {} time;
inline constexpr struct speed : quantity_spec<length / time> {} speed;
inline constexpr struct metre : unit<"m", kind_of<isq::length>> {} metre;
inline constexpr struct second : unit<"s", kind_of<isq::time>> {} second;Please also note, that we’ve added
point_quantityXXX alternatives as we
consider replacing quantity_point<..., Rep>
with quantity<point<...>, Rep>.
In such a case, we could still need some_quantity = delta_quantity || point_quantity.
Modern C++ physical quantities and units libraries use opaque types to improve the user experience while analyzing compile-time errors or inspecting types in a debugger. This is a huge usability improvement over the older libraries that use aliases to refer to long instantiations of class templates.
Having such strong types for entities is not enough. While doing arithmetics on them, we get derived entities, and they also should be easy to understand and correlate with the code written by the user. This is where symbolic expressions come into play.
The library should use the same unified approach to represent the results of arithmetics on all kinds of entities. It is worth mentioning that a generic purpose symbolic expressions library is not a good solution for a physical quantities and units library.
Let’s assume that we want to represent the results of the following two unit equations:
metre / second * secondmetre * metre / metreBoth of them should result in a type equivalent to
metre. A general-purpose library
will probably result with the types similar to the below:
mul<div<metre, second>, second>div<mul<metre, metre>, metre>Comparing such types for equivalence would not only be very expensive at compile-time but would also be really confusing to the users observing them in the compilation logs. This is why we need a dedicated solution here.
In a physical quantities and units library, we need symbolic expressions to express the results of
If the above equation results in a derived entity, we must create a type that clearly describes what we are dealing with. We need to pack a simplified expression template into some container for that. There are various possibilities here. The table below presents the types generated from unit expressions by two leading products on the market in this subject:
Unit
|
||
|---|---|---|
N⋅m |
derived_unit<metre, newton> |
UnitProduct<Meters, Newtons> |
1/s |
derived_unit<one, per<second>> |
Pow<Seconds, -1> |
km/h |
derived_unit<kilo_<metre>, per<hour>> |
UnitProduct<Kilo<Meters>, Pow<Hours, -1>> |
kg⋅m²/(s³⋅K) |
derived_unit<kilogram, pow<metre, 2>, per<kelvin, power<second, 3>>> |
UnitProduct<Pow<Meters, 2>, Kilo<Grams>, Pow<Seconds, -3>, Pow<Kelvins, -1>> |
m²/m |
metre |
Meters |
km/m |
derived_unit<kilo_<metre>, per<metre>> |
UnitProduct<Pow<Meters, -1>, Kilo<Meters>> |
m/m |
one |
UnitProduct<> |
It is a matter of taste which solution is better. While discussing
the pros and cons here, we should remember that our users often do not
have a scientific background. This is why we recommend to use syntax
that is as similar to the correct English language as possible. It
consistently uses the derived_
prefix for types representing derived units, dimensions, and quantity
specifications. Those are instantiated first with the contents of the
numerator followed by the entities of the denominator (if present)
enclosed in the per<...>
symbolic expression.
The arithmetics on units, dimensions, and quantity types require a special identity value. Such value can be returned as a result of the division of the same entities, or using it should not modify the symbolic expression on multiplication.
We chose the following names here:
one in the domain of units,dimension_one in the domain of
dimensions,dimensionless in the domain of
quantity types.The above names were selected based on the following quote from [ISO/IEC 80000]:
A quantity whose dimensional exponents are all equal to zero has the dimensional product denoted A0B0C0… = 1, where the symbol 1 denotes the corresponding dimension. There is no agreement on how to refer to such quantities. They have been called dimensionless quantities (although this term should now be avoided), quantities with dimension one, quantities with dimension number, or quantities with the unit one. Such quantities are dimensionally simply numbers. To avoid confusion, it is helpful to use explicit units with these quantities where possible, e.g., m/m, nmol/mol, rad, as specified in the SI Brochure.
The table below presents all the operations that can be done on units, dimensions, and quantity types in a quantities and units library. The right column presents corresponding expression templates being their results:
Operation
|
Resulting template expression arguments
|
|---|---|
A * B |
A, B |
B * A |
A, B |
A * A |
power<A, 2> |
{identity} * A |
A |
A * {identity} |
A |
A / B |
A, per<B> |
A / A |
{identity} |
A / {identity} |
A |
{identity} / A |
{identity}, per<A> |
pow<2>(A) |
power<A, 2> |
pow<2>({identity}) |
{identity} |
sqrt(A)
or pow<1, 2>(A) |
power<A, 1, 2> |
sqrt({identity})
or pow<1, 2>({identity}) |
{identity} |
To limit the length and improve the readability of generated types, there are many rules to simplify the resulting symbolic expression.
Ordering
The resulting comma-separated arguments of multiplication are always sorted according to a specific predicate. This is why:
static_assert(A * B == B * A);
static_assert(std::is_same_v<decltype(A * B), decltype(B * A)>);This is probably the most important of all the steps, as it allows comparing types and enables the rest of the simplification rules.
User-provided symbols (when available) are not guaranteed to be
unique in the project. For example, someone may use "s" as a
symbol for a count of samples, which, when used in a unit expression
with seconds, would cause fatal consequences (e.g., sample * second
would yield s², or sample / second
would result in one).
This is why the library chose to use type name identifiers in such cases. As of today, it could be implementation-defined of how a specific implementation orders the identifiers on a type list. If [P2830R10] gets standardized, then it will be possible for every implementation to guarantee the same ordering of types.
Aggregation
In case two of the same type identifiers are found next to each other on the argument list, they will be aggregated in one entry:
Before
|
After
|
|---|---|
A, A |
power<A, 2> |
A, power<A, 2> |
power<A, 3> |
power<A, 1, 2>, power<A, 2> |
power<A, 5, 2> |
power<A, 1, 2>, power<A, 1, 2> |
A |
Simplification
In case two of the same type identifiers are found in the numerator and denominator argument lists, they are being simplified into one entry:
Before
|
After
|
|---|---|
A, per<A> |
{identity} |
power<A, 2>, per<A> |
A |
power<A, 3>, per<A> |
power<A, 2> |
A, per<power<A, 2>> |
{identity}, per<A> |
It is important to notice here that only the elements with exactly
the same type are being simplified. This means that, for example,
m/m results
in one, but
km/m will
not be simplified. The resulting derived unit will preserve both symbols
and their relative magnitude. This allows us to properly print symbols
of some units or constants that require such behavior. For example, the
Hubble constant is expressed in
km⋅s⁻¹⋅Mpc⁻¹, where both
km and
Mpc are units of
length.
Also, to prevent possible issues in compile-time logic, all of the
library’s entities must be marked
final. This
prevents the users to derive own strong types from them, which would
prevent symbolic expression simplification of equivalent entities.
In [mp-units] library, we’ve tried to refine symbolic expressions simplification rules to preserve the information of the origin. However, we were not satisfied with the results. The generated types were much longer and harder to reason about, which decreased the compile-time errors user experience. We’ve also got issues with basic library operations (e.g., determining the best common unit). More details can be found in Refining symbolic expressions simplification rules discussion.
Repacking
In case an expression uses two results of some other operations, the components of its arguments are repacked into one resulting type and simplified there.
For example, assuming:
constexpr auto X = A / B;then:
Operation
|
Resulting template expression arguments
|
|---|---|
X * B |
A |
X * A |
power<A, 2>, per<B> |
X * X |
power<A, 2>, per<power<B, 2>> |
X / X |
{identity} |
X / A |
{identity}, per<B> |
X / B |
A, per<power<B, 2>> |
Thanks to all of the steps described above, a user may write the code like this one:
using namespace si::unit_symbols;
quantity speed = isq::speed(60. * km / h);
quantity duration = 8 * s;
quantity acceleration1 = speed / duration;
quantity acceleration2 = isq::acceleration(acceleration1.in(m / s2));
std::cout << "acceleration: " << acceleration1 << " (" << acceleration2 << ")\n";the text output provides:
acceleration: 7.5 km h⁻¹ s⁻¹ (2.08333 m/s²)
The above program will produce the following types for acceleration quantities:
acceleration1
quantity<reference<derived_quantity_spec<isq::speed, per<isq::time>>,
derived_unit<si::kilo_<si::metre>, per<non_si::hour, si::second>>>{},
double>acceleration2
quantity<reference<isq::acceleration,
derived_unit<si::metre, per<power<si::second, 2>>>>{},
double>>Modern C++ physical quantities and units library should expose compile-time constants for units, dimensions, and quantity types. Each of such constants should be of a different type. Said otherwise, every unit, dimension, and quantity type has a unique type and a compile-time instance. This allows us to do regular algebra on such identifiers and get proper types as results of such operations.
The operations exposed by such a library should include at least:
newton * metre),metre / second),pow<2>(metre)
or pow<1, 2>(metre * metre)).To improve the usability of the library, we also recommend adding:
sqrt(metre * metre)
as equivalent to pow<1, 2>(metre * metre)),cbrt(metre * metre * metre)
as equivalent to pow<1, 3>(metre * metre * metre)),inverse(second)
as equivalent to
one / second).Additionally, for units only, to improve the readability of the code, it makes sense to expose the following:
square(metre)
is equivalent to pow<2>(metre)),cubic(metre)
is equivalent to pow<3>(metre)).The above two functions could also be considered for dimensions and
quantity types. However, cubic(length)
does not seem to make much sense, and probably pow<3>(length)
should be preferred instead.
Please note that we want to keep most of the unit magnitude’s
interface implementation-defined. This is why we provide only a
minimal mandatory interface for them. For example, we have introduced a
mag_power<Basis, Num, Den = 1>
helper to get a power of a magnitude. With that, a user should probably
never need to reach for an alternative pow<Num, Den>(mag<Base>)
version. However, the latter could be considered more consistent with
the same operation done on other abstractions. Let’s compare how a unit
can be defined using both of those syntaxes:
mag_power: inline constexpr struct electronvolt :
named_unit<"eV", mag_ratio<1'602'176'634, 1'000'000'000> * mag_power<10, -19> * si::joule> {} electronvolt;pow<>(mag<>): inline constexpr struct electronvolt :
named_unit<"eV", mag_ratio<1'602'176'634, 1'000'000'000> * pow<-19>(mag<10>) * si::joule> {} electronvolt;Even though it might be inconsistent with operations on other
abstractions, we’ve decided to use the first one as it seems easier to
read and better resembles what we write on paper. However, we are not
married to it, and we can change it if the LEWG prefers consistency
here. Please note, that in such a case, for consistency, we probably
should also provide
sqrt() and
cbrt()
operations. However, those are really rare operations for magnitudes (we
have not found any use cases for those in [mp-units] so far).
Units, their magnitudes, dimensions, quantity types, and references
can be checked for equality with operator==.
Equality for all the tag types is a simple check if both arguments are
of the same type. For example, for dimensions, we do the following:
template<Dimension Lhs, Dimension Rhs>
consteval bool operator==(Lhs lhs, Rhs rhs)
{
return is_same_v<Lhs, Rhs>;
}Equality for references is a bit more complex:
template<typename Q1, typename U1, typename Q2, typename U2>
consteval bool operator==(reference<Q1, U1>, reference<Q2, U2>)
{
return is_same_v<reference<Q1, U1>, reference<Q2, U2>>;
}
template<typename Q1, typename U1, Unit U2>
consteval bool operator==(reference<Q1, U1>, U2 u2)
{
return Q1{} == get_quantity_spec(u2) && U1{} == u2;
}The second overload allows us to mix associated units and
specializations of reference class
template (both of them satisfy
Reference concept). Thanks to this,
we can check the following:
static_assert(isq::time[second] != second);
static_assert(kind_of<isq::time>[second] == second);Units may have many shades. This is why an equality check is not
enough for them. In many cases, we don’t need to check against a
concrete unit, but we want to ensure that the underlying numerical value
will not change during a unit conversion. In such cases we check for
equivalence. Watt (W) should be
equivalent to
J/s and
kg m²/s³.
Also, a litre (l) should be
equivalent to a cubic decimetre
(dm³).
To check for unit equivalence, currently we convert each unit to its canonical representation (scaled unit with magnitude expressed relative to some “blessed” implementation-specific reference unit) and then, we compare if the reference units and the magnitudes are the same:
consteval bool equivalent(Unit auto lhs, Unit auto rhs)
{
const auto lhs_canonical = get_canonical_unit(lhs);
const auto rhs_canonical = get_canonical_unit(rhs);
return lhs_canonical.mag == rhs_canonical.mag && lhs_canonical.reference_unit == rhs_canonical.reference_unit;
}Note: A canonical_unit is an
implementation detail and is not exposed in public APIs for
now.
For example:
static_assert(N != kg * m / s2);
static_assert(equivalent(N, kg * m / s2));It is also worth noting that the above implementation makes the last
line below pass, even though we can’t convert a quantity measured in
Hz to the one in
Bq:
quantity q1 = (42 * Hz).in(one / s);
quantity q2 = (42 * Bq).in(one / s);
quantity q3 = (42 * one / s).in(Hz);
quantity q4 = (42 * one / s).in(Bq);
// quantity q5 = (42 * Hz).in(Bq); // does not compile
quantity q6 = 1 * Hz + 1 * one / s;
quantity q7 = 1 * Bq + 1 * one / s;
// quantity q8 = 1 * Hz + 1 * Bq; // does not compile
static_assert(Hz != Bq);
static_assert(Hz != one / s);
static_assert(Bq != one / s);
static_assert(equivalent(Hz, one / s));
static_assert(equivalent(Bq, one / s));
static_assert(equivalent(Hz, Bq)); // OK ???Depending on the desired semanthics of
equivalent function, we may want to
make the last line to fail as well.
Ordering for dimensions and quantity types has no physical sense.
We could entertain adding ordering for units, but this would work only for quantities having the same reference unit, which would be inconsistent with how equality works.
Let’s see the following example:
constexpr Unit auto my_unit = si::second;
if constexpr (my_unit == si::metre) {
// ...
}
if constexpr (my_unit > si::metre) {
// ...
}
if constexpr (my_unit > si::nano(si::second)) {
// ...
}In the above code, the first check could be useful for some use cases. However, the second one is impossible to implement and should not compile. The third one could be considered useful, but the current version of [mp-units] does not expose such an interface to limit potential confusion. Also, it is really hard to mathematically prove that the unit magnitude representation that we use in the library (based on primes factorization) is greater or smaller than the other one in some cases.
This is why we discourage providing ordering operations for any of those entities.
For consistency, we could also define arithmetic operator+
and operator-
for such entities to resemble the operations performed on quantities.
For example:
quantity q1 = isq::radius(1 * m) + isq::distance(1 * cm);
quantity q2 = isq::position_vector(1 * m) - isq::position_vector(1 * cm);
// quantity q3 = isq::position_vector(1 * m) + isq::position_vector(1 * cm); // should not compilereturns:
quantity<isq::length[cm], int>
for q1,quantity<isq::displacement[cm], int>
for q2.Users may be interested to check what will be the result of performing such operations on ingredients of the quantity. As we say that “addition of a radius and a distance should yield a length” it would be good to model this arithmetics on our symbolic constants as well:
static_assert(isq::radius + isq::distance == isq::length);
static_assert(isq::position_vector - isq::position_vector == isq::displacement);
// constexpr auto qs = isq::position_vector + isq::position_vector; // should not compileThe operations that we expose must cover all of the operations we can do on quantities. This is why we not only have to overload operators but also expose other operations that can be performed on vector, tensor, and complex quantities:
static_assert(implicitly_convertible(magnitude(isq::velocity), isq::speed));
static_assert(implicitly_convertible(scalar_product(isq::force, isq::displacement), isq::work));
static_assert(implicitly_convertible(vector_product(isq::position_vector, isq::force), isq::moment_of_force));
static_assert(implicitly_convertible(real(isq::complex_power), isq::active_power));
static_assert(implicitly_convertible(imag(isq::complex_power), isq::reactive_power));
static_assert(implicitly_convertible(modulus(isq::complex_power), isq::apparent_power));Addition and subtractions on units is also possible, but it is more controverisal and less useful, so we do not propose them at this time:
static_assert(m + cm == cm);
static_assert(km + mi == get_common_unit(km, mi));ISO specifies a measurement unit as a real scalar quantity, defined and adopted by convention, with which any other quantity of the same kind can be compared to express the ratio of the two quantities as a number.
In other words, a unit is a specific amount of a quantity. Such a definition is impractical from the programming language point of view. Let’s see the following hypothetical example (the below API is not a part of this proposal):
namespace si {
constexpr auto metre = quantity<length>{1};
constexpr auto kilometre = 1000 * metre;
}
quantity<si::kilometre> distance = 42 * si::kilometre;The above code would be consistent with the ISO definition however, it imposes several issues:
quantity class,quantity<length>{1}
may mean different things in namespaces of different systems which makes
it much harder to provide interoperability between them,This is why decided to base unit definitions on tag types.
space_before_unit_symbol
alternativesAs described in the space_before_unit_symbol
customization point chapter, some units should not be prepended with
a space. We proposed the following customization point:
template<Unit auto U>
constexpr bool space_before_unit_symbol = true;It is important to note that the need for some customization is only for a small fraction of all units. It works but it has some disadvantages. First, it might be harder to reason about the units definitions because the spacialization of this variable template may be in a different location in the source code than the unit definition. Also, it breaks our assumption that we can define all the properties of the entity with a single line of a C++ code.
Maybe we should add an additional parameter (defaulted to
true) to the
named_unit class template to handle
this?
Initially [mp-units] library had one additional customization point for units:
template<PrefixableUnit auto U>
constexpr bool unit_can_be_prefixed = true;The above was used to disallow prefixes for some units, such as hours or degrees Celsius. However, after some time, we got the issue on GitHub asking to allow prefixes for the latter.
It turns out that the certification organizations are not consistent here. ISO 80000-5 says:
Prefixes are not allowed in combination with the unit °C.
However, NIST states:
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. However, to avoid confusion, prefix symbols (and prefix names) are not used with the time-related unit symbols (names) min (minute), h (hour), d (day); nor with the angle-related symbols (names) º (degree), ’ (minute), and ” (second).
As a result of this issue and associated discussion, we decided to
remove unit_can_be_prefixed support
from the library, and we do not propose it here either.
[ Note: The word “magnitude” appears in this paper with three distinct meanings:
|v|,
provided by the
norm() CPO
(also accessible as magnitude(v)
for compatibility with physics terminology).Each unit is associated with a magnitude representing its scaling factor relative to other units of the same dimension. However, absolute magnitude values have no physical meaning—only the ratio between magnitudes matters. For example, once we assign magnitude \(m_f\) to foot, we must assign \(3m_f\) to yard and \(m_f/12\) to inch.
We make magnitude interfaces mostly implementation-defined, exposing only minimal public APIs for interoperability while leaving freedom to implementers.
std::ratioMagnitudes must support operations that units require: products and rational powers. Additionally, they must handle irrational ratios like \(\frac{\pi}{180}\) between degrees and radians.
std::ratio
fails these requirements:
The solution uses prime factorization as a vector space basis. Each magnitude is a product of prime powers, with irrational constants (like \(\pi\)) added as additional basis elements when needed.
Examples using Astronomical Units (au), meters (m), degrees (deg), and radians (rad):
Unit ratio
|
std::ratio
|
Vector space magnitude
|
|---|---|---|
| \(\left(\frac{\text{au}}{\text{m}}\right)\) | std::ratio<149'597'870'700> |
magnitude<power_v<2, 2>(), 3, power_v<5, 2>(), 73, 877, 7789> |
| \(\left(\frac{\text{au}}{\text{m}}\right)^2\) | Overflow | magnitude<power_v<2, 4>(), power_v<3, 2>(), power_v<5, 4>(), power_v<73, 2>(), power_v<877, 2>(), power_v<7789, 2>()> |
| \(\sqrt{\frac{\text{au}}{\text{m}}}\) | Unrepresentable | magnitude<2, power_v<3, 1, 2>(), 5, power_v<73, 1, 2>(), power_v<877, 1, 2>(), power_v<7789, 1, 2>()> |
| \(\left(\frac{\text{rad}}{\text{deg}}\right)\) | Unrepresentable | magnitude<power_v<2, 2>(), power_v<3, 2>(), power_v<pi_c{}, -1>(), 5> |
Trade-offs: more verbose type names (mitigated by opaque types) and dependency on compile-time prime factorization.
Users write mag<149'597'870'700>,
which the library expands to its prime factorization. Large primes
(e.g., 334,524,384,739 in the proton mass) cause compilers to assume
infinite loops and terminate compilation when using trial division.
[P3133R0] explored std::first_factor(uint64_t)
as a solution. Feedback showed a fast primality checker suffices for
practical cases, though the function would still benefit the standard
library and other domains.
Computing common magnitude: for each basis vector, take the minimum exponent across participating magnitudes (using implicit “0” for omitted vectors).
Example: \(\text{COM}[18, \frac{80}{3}] = \text{COM}[(2 \cdot 3^2), (2^4 \cdot 3^{-1} \cdot 5)] = 2^{\min[1,4]} \cdot 3^{\min[2,-1]} \cdot 5^{\min[0,1]} = \frac{2}{3}\)
Physical constants are implemented as units rather than
constexpr
quantity values. Benefits:
Example definitions:
namespace si {
namespace si2019 {
inline constexpr struct speed_of_light_in_vacuum :
named_constant<"c", mag<299'792'458> * metre / second> {} speed_of_light_in_vacuum;
} // namespace si2019
inline constexpr struct magnetic_constant :
named_constant<{u8"μ₀", "u_0"}, mag<4> * mag_power<10, -7> * π * henry / metre> {} magnetic_constant;
} // namespace siUsage example (vacuum permittivity):
constexpr auto permeability_of_vacuum = 1. * si::magnetic_constant;
constexpr auto speed_of_light_in_vacuum = 1 * si::si2019::speed_of_light_in_vacuum;
QuantityOf<isq::permittivity_of_vacuum> auto q = 1 / (permeability_of_vacuum * pow<2>(speed_of_light_in_vacuum));
std::cout << q << " = " << q.in(F / m) << "\n"; // prints: 1 μ₀⁻¹ c⁻² = 8.85419e-12 F/mNamed units may not be enough to model all of the constants out there. It turns out that there are many negative constants. Some of them can be found in CODATA. One such constant is helion g factor.
Trying to model this with
named_unit fails to compile. The
reason of the error is the fact that the conversion factors between
units should be positive. This means that reusing
named_unit to define constants may
not be the best idea and we probably need to introduce a dedicated
class.
This is why we need to introduce a new class template:
inline constexpr struct helion_g_factor :
named_constant<basic_symbol_text{"𝘨ₕ", "g_h"}, mag<-ratio{4'255'250'615, 1'000'000'000}> * one> {} helion_g_factor;Additionally, such a solution does not allow the constant to be
prefixed or associated with a
quantity_spec.
Quantity specification provides all the data about the quantity type (i.e., kind, character, recipe, relation to other quantities in the hierarchy). It does not specify a unit, though.
quantity_specThe “quantity specification” term is not provided in ISO or BIPM metrology dictonaries and was invented for the need of this library. This means that we should probably consider some other names for this abstraction:
quantity_specification,q_spec,q_specification,quantity_definition,quantity_def,quantity_data,q_data.Note: We know that probably the term “reference” will not survive too long in the Committee, but we couldn’t find a better name for it in the [mp-units] library (https://github.com/mpusz/mp-units/issues/486).
[ISO/IEC Guide 99] says:
quantity - property of a phenomenon, body, or substance, where the property has a magnitude that can be expressed as a number and a reference. … A reference can be a measurement unit, a measurement procedure, a reference material, or a combination of such.
In the library a quantity reference represents all the
domain-specific meta-data about the quantity besides its representation
type and its value. A Reference
concept is satisfied by either of:
si::metre),reference<QuantitySpec, Unit>
class template explicitly specifying the quantity type and its
unit.A reference type is implicitly created as a result of the following expression:
constexpr Reference auto distance = isq::distance[m];The above example defines a variable of type reference<isq::distance, si::metre>.
The reference class template also
exposes an arithmetic interface similar to the one that we have already
discussed in case of units and quantity types. It simply forwards the
operation to its quantity type and unit members.
constexpr ReferenceOf<isq::speed> auto speed = distance / si::second;As a result we get a reference<derived_quantity_spec<distance, per<time>>, derived_unit<metre, per<second>>>
type.
Similarly to the Unit, such
a reference can be used to construct a quantity:
QuantityOf<isq::speed> auto s = 60 * speed;referenceThe term reference is highly
overloaded in the C++ domain. This is why we should probably rename the
type that was successfully used in [mp-units]. Here are a few
proposals:
quantity_referencequantity_refq_referenceq_refPlease note that the longer the identifier we choose, the longer and
harder it will be to grasp compiler error messages. A user never types
this type identifier in the code (although a user might type an
associated concept Reference or
ReferenceOf).
The quantity class template is a
workhorse of the library. It can be considered a generalization of std::chrono::duration,
but is not directly compatible with it.
Based on the ISO definition provided in the Quantity references chapter, the
quantity class template has the
following signature:
template<Reference auto R, RepresentationOf<get_quantity_spec(R)> Rep = double>
class quantity;It stores only one data member of
Rep type. Unfortunately, this data
member has to be publicly exposed to satisfy the C++ language
requirements for structural
types. Hopefully, the language rules for structural types will
improve with time before this library gets standardized.
As of today, the multiply syntax that creates quantities is not commutative:
quantity q1 = 1 * m; // OK
quantity q2 = m * 1; // Compile-time errorWe decided to go this way to increase the readability of the code and limit possible confusion with this syntax. After a while, we extended it to support also the following:
quantity q3 = 1 * m / s; // OK
quantity q4 = 1 * m * m; // OK
quantity q5 = 1 / s * m; // OK
quantity q6 = s / 2; // Compile-time error
quantity q7 = m * (1 / s); // Compile-time error
quantity q8 = m * (1 * m); // Compile-time errorHowever, [mp-units] users requested the following use case:
if(num < Unit / 1'000'000'000'000) {
quantity<si::femto<Unit>, double> n{num};
out << n;
} else if(num < Unit / 1'000'000'000) {
quantity<si::pico<Unit>, double> n{num};
out << n;
} else // ...Today, this does not compile. Should we extend the multiply syntax to support such use cases and with this have entire commutative property?
Quantity construction chapter
describes and explains why we introduced the multiply syntax as a
construction helper for quantities. Many people ask why we chose this
approach over battle-proven User Defined Literals (UDLs) that work well
for the
std::chrono
library.
It turns out that 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 besides the unit tests, only a few compile-time known quantity values are used in the production code. Please note that for Physical constants, we recommend using units rather than compile-time constants.
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 UDL is intermixed in arithmetics with any quantity type of a shorter representation type, it will always expand it to the longest one. In other words, such long type spreads until all types use it everywhere.
While increasing the coverage for the [mp-units] library, we learned that many
unit symbols conflict with built-in types or numeric extensions. A few
of those are: F (farad),
J (joule),
W (watt),
K (kelvin),
d (day),
l or
L (litre),
erg,
ergps. Using the
'_'
prefix would make it work for [mp-units], but if the library is
standardized, those naming collisions would be a big issue. This is why
we came up with the _q_ prefix that
would become q_ after
standardization (e.g., 42q_s),
which is not that nice anymore.
UDLs with the same identifiers defined in different namespace
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 in case both
namespaces are “imported” with using directives.
Another bad property 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 predefine for differently scaled versions of all unit
ingredients?
UDLs are also really expensive to define and specify. Typically, for each unit, we need two definitions. One for integral and another one for floating-point representation. In version 0.8.0 of the [mp-units] library, the coherent unit of angular momentum was defined as:
constexpr auto operator"" _q_kg_m2_per_s(unsigned long long l)
{
gsl_ExpectsAudit(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);
}The multiply syntax that we chose for this library does not have any of those issues.
quantity class template,
similarly to std::chrono::duration,
exposes some special values as
static
member functions:
min(),max(),zero().Also, similarly to std::chrono::duration
those functions are implemented in terms of a type trait:
template<typename Rep>
struct representation_values : std::chrono::duration_values<Rep> {
static constexpr Rep one() noexcept
requires std::constructible_from<Rep, int>
{
return Rep(1);
}
};An additional the
one()
function in representation_values is
provided for use with the multiply syntax when constructing quantities.
Users can create a quantity with numerical value of one using: representation_values<double>::one() * si::metre.
This function is not exposed as a static member of
quantity because
one() is not
a true multiplicative identity for dimensional quantities—for example,
pow<2>(quantity<si::metre>::one())
would change the dimension from length to area.
Please also note that in C++26, std::chrono::duration_values
is not a part of the freestanding library.
quantity is a numeric wrapperIf we think about it, the
quantity class template is just a
“smart” numeric wrapper. It exposes properly constrained set of
arithmetic operations on one or two operands.
Every single arithmetic operator is exposed by the
quantity class template only if the
underlying representation type provides it as well and its
implementation has proper semantics (e.g., returns a reasonable
type).
For example, in the following code,
-a will
compile only if MyInt exposes such
an operation as well:
quantity a = MyInt{42} * m;
quantity b = -a;Assuming that:
q is our quantity,qq is a quantity implicitly
convertible to q,q2 is any other quantity,kind is a quantity of the same
kind as q,one is a quantity of
dimension_one with the unit
one,number is a value of a type
“compatible” with q’s representation
type,here is the list of all the supported operators:
Unary
|
Compound assignment
|
Binary
|
Ordering & comparison
|
|---|---|---|---|
+q-q++qq++--qq-- |
q += qqq -= qqq %= qqq *= numberq *= oneq /= numberq /= one |
q + kindq - kindq % kindq * q2q * numbernumber * qq / q2q / numbernumber / q |
q == kindq <=> kind |
As we can see, there are plenty of operations one can do on a value
of a quantity type. As most of them
are obvious, in the following chapters, we will discuss only the most
important or non-trivial aspects of quantity arithmetics.
Quantities can easily be added or subtracted from each other:
static_assert(1 * m + 1 * m == 2 * m);
static_assert(2 * m - 1 * m == 1 * m);
static_assert(isq::height(1 * m) + isq::height(1 * m) == isq::height(2 * m));
static_assert(isq::height(2 * m) - isq::height(1 * m) == isq::height(1 * m));The above uses the same types for LHS, RHS, and the result, but in general, we can add, subtract, or compare the values of any quantity type as long as both quantities are of the same kind. The result of addition and subtraction will be the common type of the arguments:
static_assert(1 * km + 1.5 * m == 1001.5 * m);
static_assert(isq::height(1 * m) + isq::width(1 * m) == isq::length(2 * m));
static_assert(isq::height(2 * m) - isq::distance(0.5 * m) == 1.5 * m);
static_assert(isq::radius(1 * m) - 0.5 * m == isq::radius(0.5 * m));Please note that for the compound assignment operators, we always need to end up with the left-hand-side argument type:
static_assert((1 * m += 1 * km) == 1001 * m);
static_assert((isq::length(1 * m) += isq::height(1 * m)) == isq::length(1 * m));
static_assert((isq::height(1.5 * m) -= 1 * m) == isq::height(0.5 * m));If we break those rules, the code will not compile:
quantity q1 = 1 * m -= 0.5 * m; // Compile-time error (1)
quantity q2 = 1 * km += 1 * m; // Compile-time error (2)
quantity q3 = isq::height(1 * m) += isq::length(1 * m); // Compile-time error (3)(1)
Convertions of the floating-point to integral representation type is
considered narrowing.
(2)
Conversion of quantity with integral representation type from a unit of
a higher resolution to the one with a lower resolution is considered
narrowing.
(3)
Conversion from a more generic quantity type to a more specific one is
considered unsafe.
Please note that all the above operations either preserved the input representation types or returned a common type if those were different for both arguments. This is not the case for irrational conversion factors. In such cases, the library will force the user to use at least one floating-point representation type to prevent truncation:
template<typename... Ts>
consteval bool invalid_arithmetic(Ts... ts)
{
return !requires { (... + ts); } && !requires { (... - ts); };
}
static_assert(invalid_arithmetic(1 * rad, 1 * deg));
static_assert(is_of_type<1. * rad + 1 * deg, quantity<deg, double>>);
static_assert(is_of_type<1 * rad + 1. * deg, quantity<deg, double>>);
static_assert(is_of_type<1. * rad + 1. * deg, quantity<deg, double>>);Multiplying or dividing a quantity by a number does not change its quantity type or unit. However, its representation type may change. For example:
static_assert(isq::height(3 * m) * 0.5 == isq::height(1.5 * m));Unless we use a compound assignment operator, in which case we always have to result with the type of the left-hand-side argument. This, together with the fact that this library tries to prevent truncation of a quantity value means, that the following does not compile:
quantity q = isq::height(3 * m) *= 0.5; // Compile-time errorHowever, suppose we multiply or divide quantities of the same or different types, or we divide a raw number by a quantity. In that case, we most probably will end up in a quantity of yet another type:
static_assert(120 * km / (2 * h) == 60 * km / h);
static_assert(isq::width(2 * m) * isq::length(2 * m) == isq::area(4 * m2));
static_assert(50 / isq::time(1 * s) == isq::frequency(50 * Hz));An exception from the above rule happens when one of the arguments is
a dimensionless quantity. If we multiply or divide by such a quantity,
the quantity type will not change. If such a quantity has a unit
one, also the unit of a quantity
will not change:
static_assert(120 * m / (2 * one) == 60 * m);An interesting special case happens when we divide the same quantity kinds or multiply a quantity by its inverted type. In such a case, we end up with a dimensionless quantity.
static_assert(isq::height(4 * m) / isq::width(2 * m) == 2 * one); // (1)
static_assert(5 * h / (120 * min) == 0 * one); // (2)
static_assert(5. * h / (120 * min) == 2.5 * one);(1)
The resulting quantity type of the LHS is isq::height / isq::width,
which is a quantity of the dimensionless kind.
(2)
The resulting quantity of the LHS is 0 * dimensionless[h / min].
To be consistent with the division of different quantity types, we do
not convert quantity values to a common unit before the division.
The physical units library can’t do any runtime branching logic for the division operator. All logic has to be done at compile-time when the actual values are not known, and the quantity types can’t change at runtime.
If we expect 120 * km / (2 * h)
to return 60 km/h,
we have to agree with the fact that 5 * km / (24 * h)
returns 0 km/h.
We can’t do a range check at runtime to dynamically adjust scales and
types based on the values of provided function arguments.
This is why we often prefer floating-point representation types when dealing with units. Some popular physical units libraries even forbid integer division at all.
Now that we know how addition, subtraction, multiplication, and division work, it is time to talk about modulo. What would we expect to be returned from the following quantity equation?
auto q = 5 * h % (120 * min);Most of us would probably expect to see
1 h or
60 min as a
result. And this is where the problems start.
The C++ language defines its
/ and
% operators
with the quotient-remainder
theorem:
q = a / b;
r = a % b;
q * b + r == a;
The important property of the modulo operator is that it only works for integral representation types (it is undefined what modulo for floating-point types means). However, as we saw in the previous chapter, integral types are tricky because they often truncate the value.
From the quotient-remainder theorem, the result of modulo operation
is r = a - q * b.
Let’s see what we get from such a quantity equation on integral
representation types:
quantity a = 5 * h;
quantity b = 120 * min;
quantity q = a / b;
quantity r = a - q * b;
std::cout << "reminder: " << r << "\n";The above code outputs:
reminder: 5 h
And now, a tough question needs an answer. Do we really want modulo
operator on physical units to be consistent with the quotient-remainder
theorem and return
5 h for
5 * h % (120 * min)?
This is exactly why we decided not to follow this hugely surprising path in this library. The selected approach was also consistent with the feedback from C++ experts. For example, this is what Richard Smith said about this issue:
I think the quotient-remainder property is a less important motivation here than other factors – the constraints on
%and/are quite different, so they lack the inherent connection they have for integers. In particular, I would expect thatA / Bworks for all quantitiesAandB, whereasA % Bis only meaningful whenAandBhave the same dimension. It seems like a nice-to-have for the property to apply in the case where both/and%are defined, but internal consistency of/across all cases seems much more important to me.I would expect
61 min % 1 hto be1 min, and1 h % 59 minto also be1 min, so my intuition tells me that the result type ofA % B, whereAandBhave the same dimension, should have the smaller unit ofAandB(and if the smaller one doesn’t divide the larger one, we should either use thegcd / std::common_typeof the units ofAandBor perhaps just produce an error). I think any other behavior for%is hard to defend.On the other hand, for division it seems to me that the choice of unit should probably not affect the result, and so if we want that
5 mm / 120 min = 0 mm/min, then5 h / 120 min == 0 hc(wherehcis a dimensionless “hexaconta”, or60x, unit). I don’t like the idea of taking SI base units into account; that seems arbitrary and like it would do the wrong thing as often as it does the right thing, especially when the units have a multiplier that is very large or small. We could special-case the situation of a dimensionless quantity, but that could lead to problematic overflow pretty easily: a calculation such as10 s * 5 GHz * 2 uWwould overflow anintif it produces a dimensionless quantity for10 s * 5 GHz, but it could equally produce50 G * 2 uW = 100 kWwithout any overflow, and presumably would if the terms were merely reordered.If people want to use integer-valued quantities, I think it’s fundamental that you need to know what the units of the result of an operation will be, and take that into account in how you express computations; the simplest rule for heterogeneous operators like
*or/seems to be that the units of the result are determined by applying the operator to the units of the operands – and for homogeneous operators like+or%, it seems like the only reasonable option is that you get thestd::common_typeof the units of the operands.
To summarize, the modulo operator on physical units has more in common with addition and division operators than with the quotient-remainder theorem. To avoid surprising results, the operation uses a common unit to do the calculation and provide its result:
static_assert(5 * h / (120 * min) == 0 * one);
static_assert(5 * h % (120 * min) == 60 * min);
static_assert(61 * min % (1 * h) == 1 * min);
static_assert(1 * h % (59 * min) == 1 * min);Zero is special. It is the only number that unambiguously defines the value of any kind of quantity, regardless of its units: zero inches and zero meters and zero miles are all identical. For this reason, it’s very common to compare the value of a quantity against zero. For example, when checking the sign of a quantity, or when making sure that it’s nonzero.
We could implement such checks in the following way:
if(q1 / q2 != 0 * m / s)
// ...The above would work (assuming we are dealing with the quantity of
speed), but it’s not ideal. If the result of
q1 / q2 is
not expressed in
m / s, we’ll
incur an extra unit conversion. Even if it is in
m / s, it’s
cumbersome to repeat the unit in a context where it makes no
difference.
We could avoid repeating the unit, and guarantee there won’t be an extra conversion, by writing:
if(auto q = q1 / q2; q != q.zero())
// ...But that is a bit inconvenient, and inexperienced users could be unaware of this technique and its rationale.
For the above reasons, the library provides special support for
comparisons against the literal
0. Only this
one value has elevated privileges and does not have to state the unit —
the numerical value zero is common to all scaled units of any kind.
Thanks to that, to save typing and not pay for unneeded conversions, our check could be implemented as follows:
if (q1 / q2 != 0)
// ...All six comparison operators support comparison against zero without
specifying a unit. This works with any representation type
rep for which representation_values<rep>::zero()
is provided.
Only a compile-time zero is accepted: an integer or floating-point
literal that is zero (e.g.,
0,
0., 0.f,
0LL).
Passing another literal or a runtime variable — even one whose value
happens to be zero — is rejected at compile time.
This chapter scoped only on the
quantity type’s operators. However,
there are many named math functions provided in the [mp-units] library. Among others, we can
find there the following:
pow(),
sqrt(),
cbrt(),exp(),abs(),epsilon(),fma(),
fmod(),
remainder(),isfinite(),
isinf(),
isnan(),floor(),
ceil(),
round(),inverse(),hypot(),sin(),
cos(),
tan(),asin(),
acos(),
atan(),
atan2().In the library, we can also find the <mp-units/random.h>
header file with all the pseudo-random number generators.
We plan to provide a separate paper on those in the future.
The quantities we discussed so far always had some specific type and physical dimension. However, this is not always the case. While performing various computations, we sometimes end up with so-called “dimensionless” quantities, which [ISO/IEC Guide 99] correctly defines as quantities of dimension one:
- Quantity for which all the exponents of the factors corresponding to the base quantities in its quantity dimension are zero.
- The measurement units and values of quantities of dimension one are numbers, but such quantities convey more information than a number.
- Some quantities of dimension one are defined as the ratios of two quantities of the same kind.
- Numbers of entities are quantities of dimension one.
Dividing two quantities of the same kind always results in a quantity of dimension one. However, depending on what type of quantities we divide or what their units are, we may end up with slightly different types.
Dividing two quantities of the same dimension always results in a
quantity with the dimension being
dimension_one. This is often
different for other physical units libraries, which may return a raw
representation type for such cases. A raw value is also always returned
from the division of two std::chrono::duration
values.
In the initial design of the [mp-units] library, the resulting type
of division of two quantities was their common representation type (just
like std::chrono::duration):
static_assert(std::is_same_v<decltype(10 * km / (5 * km)), int>);The reasoning behind it was not providing a false impression of a
strong quantity type for something
that looks and feels like a regular number. Also, all of the mathematic
and trigonometric functions were working fine out of the box with such
representation types, so we did not have to rewrite
sin(),
cos(),
exp(), and
others.
However, the feedback we got from the production usage was that such
an approach is really bad for generic programming. It is hard to handle
the result of the two quantities’ division (or multiplication) 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
(units::pow
takes the power as template arguments). Those are only 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 such case, we may end up with a really small or a huge floating-point value, which may result in losing lots 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.
First, let’s analyze what happens if we divide two quantities of the same type:
constexpr QuantityOf<dimensionless> auto q = isq::height(200 * m) / isq::height(50 * m);In such a case, we end up with a dimensionless quantity that has the following properties:
static_assert(q.quantity_spec == dimensionless);
static_assert(q.dimension == dimension_one);
static_assert(q.unit == one);In case we would like to print its value, we would see a raw value of
4 in the
output with no unit being printed.
We can divide quantities of the same dimension and unit but of different quantity types:
constexpr QuantityOf<dimensionless> auto q = isq::work(200 * J) / isq::heat(50 * J);Again we end up with
dimension_one and
one, but this time:
static_assert(q.quantity_spec == isq::work / isq::heat);As shown above, the result is not of a
dimensionless type anymore. Instead,
we get a quantity type derived from the performed quantity equation.
According to the [ISO/IEC 80000], work divided
by heat is the recipe for the thermodynamic efficiency
quantity, thus:
static_assert(implicitly_convertible(q.quantity_spec, isq::efficiency_thermodynamics));Please note that the quantity of isq::efficiency_thermodynamics
is of a kind dimensionless, so it is
implicitly convertible to
dimensionless and satisfies the QuantityOf<dimensionless>
concept.
Now, let’s see what happens when we divide two quantities of the same type but different units:
constexpr QuantityOf<dimensionless> auto q = isq::height(4 * km) / isq::height(2 * m);This time we get a quantity of
dimensionless type with a
dimension_one as its dimension.
However, the resulting unit is not
one anymore:
static_assert(q.unit == mag_power<10, 3> * one);In case we would print the text output of this quantity, we would not
see a raw value of
2000, but
2 km/m.
First, it may look surprising, but this is actually consistent with
the division of quantities of different dimensions. For example, if we
divide 4 * km / (2 * s),
we do not expect km to be “expanded”
to m before the division, right? We
would expect the result of 2 * (km / s),
which is exactly what we get when we divide quantities of the same
kind.
This is a compelling feature that allows us to express huge or tiny ratios without the need for big and expensive representation types. With this, we can easily define things like a Hubble’s constant that uses a unit that is proportional to the ratio of kilometers per megaparsecs, which are both units of length:
inline constexpr struct hubble_constant :
named_constant<{u8"H₀", "H_0"}, mag_ratio<701, 10> * si::kilo<si::metre> / si::second / si::mega<parsec>> {
} hubble_constant;Another important use case for dimensionless quantities is to provide strong types for counts of things. For example:
Thanks to assigning strong names to such quantities, they can be used
in the quantity equation of other quantities. For example,
rotational frequency is defined by rotation / duration.
As we observed above, the most common unit for dimensionless
quantities is one. It has the ratio
of 1 and
does not output any textual symbol.
A unit one is special in the
entire type system of units as it is considered to be an identity
operand in the unit symbolic expressions. This means that, for
example:
static_assert(one * one == one);
static_assert(one * si::metre == si::metre);
static_assert(si::metre / si::metre == one);The same is also true for
dimension_one and
dimensionless in the domains of
dimensions and quantity specifications, respectively.
Besides the unit one, there are a
few other scaled units predefined in the library for usage with
dimensionless quantities:
inline constexpr struct percent : named_unit<"%", mag_ratio<1, 100> * one> {} percent;
inline constexpr struct per_mille : named_unit<{u8"‰", "%o"}, mag_ratio<1, 1000> * one> {} per_mille;
inline constexpr struct parts_per_million : named_unit<"ppm", mag_ratio<1, 1'000'000> * one> {} parts_per_million;
inline constexpr auto ppm = parts_per_million;
inline constexpr struct pi : named_constant<symbol_text{u8"π" /* U+03C0 GREEK SMALL LETTER PI */, "pi"}, mag<pi_c> * one> {} pi;
inline constexpr auto π /* U+03C0 GREEK SMALL LETTER PI */ = pi;oneQuantities implicitly convertible to
dimensionless with the unit
equivalent to one are the only ones
that are:
quantity<one> inc(quantity<one> q) { return q + 1; }
void legacy(double) { /* ... */ }
if (auto q = inc(42); q != 0)
legacy(static_cast<int>(q));This property also expands to usual arithmetic operators.
Please note that 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 for quantities that are
not implicitly convertible to
dimensionless (e.g.,
angular_measure).
Special, often controversial, examples of dimensionless quantities
are the angular measure and solid angular measure
quantities that are defined in [ISO/IEC 80000] (part 3) to be the
result of a division of arc_length / radius
and area / pow<2>(radius)
respectively. Moreover, [ISO/IEC 80000] also explicitly states
that both can be expressed in the unit
one. This means that both isq::angular_measure
and isq::solid_angular_measure
should be of a kind of
dimensionless.
On the other hand, [ISO/IEC 80000] also specifies that the
unit radian can be used for
angular measure, and the unit
steradian can be used for solid
angular measure. Those should not be mixed or used to express other
types of dimensionless quantities. We should not be able to measure:
This means that both isq::angular_measure
and isq::solid_angular_measure
should also be quantity kinds by themselves.
Note: Many people claim that angle being a dimensionless quantity
is a bad idea. There are proposals submitted to make an angle a base
quantity and rad to become a base
unit in both [SI] and [ISO/IEC 80000].
Thanks to the usage of magnitudes the library provides efficient strong types for all angular types. This means that with the built-in support for magnitudes of \(\pi\) we can provide accurate conversions between radians and degrees. The library also provides common trigonometric functions for angular quantities:
quantity speed = 110 * km / h;
quantity rate_of_climb = -0.63657 * m / s;
quantity glide_ratio = speed / -rate_of_climb;
quantity glide_angle = angular::asin(1 / glide_ratio);
std::println("Glide ratio: {::N[.1f]}", glide_ratio.in(one));
std::println("Glide angle:");
std::println(" - {::N[.4f]}", glide_angle);
std::println(" - {::N[.2f]}", glide_angle.in(angular::degree));
std::println(" - {::N[.2f]}", glide_angle.in(angular::gradian));The above program prints:
Glide ratio: 48.0
Glide angle:
- 0.0208 rad
- 1.19°
- 1.33ᵍ
Angular quantities are not the only ones with such a “strange”
behavior. A similar case is the storage capacity quantity
specified in [ISO/IEC 80000] (part 13) that again
allows expressing it in both one and
bit units.
Those cases make dimensionless quantities an exceptional tree in the library. This quantity hierarchy contains more than one quantity kind and more than one unit in its tree:
Dotted lines denote is_kind
relationships, where a child quantity forms a distinct kind incompatible
with quantities of the same parent.
To provide such support in the library, we provided an
is_kind specifier that can be
appended to the quantity specification:
inline constexpr struct angular_measure : quantity_spec<dimensionless, arc_length / radius, is_kind> {} angular_measure;
inline constexpr struct solid_angular_measure : quantity_spec<dimensionless, area / pow<2>(radius), is_kind> {} solid_angular_measure;
inline constexpr struct storage_capacity : quantity_spec<dimensionless, is_kind> {} storage_capacity;With the above, we can constrain
radian,
steradian, and
bit to be allowed for usage with
specific quantity kinds only:
inline constexpr struct radian : named_unit<"rad", metre / metre, kind_of<isq::angular_measure>> {} radian;
inline constexpr struct steradian : named_unit<"sr", square(metre) / square(metre), kind_of<isq::solid_angular_measure>> {} steradian;
inline constexpr struct bit : named_unit<"bit", one, kind_of<storage_capacity>> {} bit;This still allows the usage of
one (possibly scaled) for such
quantities which is exactly what we wanted to achieve.
It is worth mentioning here that converting up the hierarchy beyond a subkind requires an explicit conversion. For example:
static_assert(implicitly_convertible(isq::rotation, dimensionless));
static_assert(!implicitly_convertible(isq::angular_measure, dimensionless));
static_assert(explicitly_convertible(isq::angular_measure, dimensionless));This increases type safety and prevents accidental quantities with
invalid units. For example, a result of a conversion from isq::angular_measure[rad]
to dimensionless would be a
reference of dimensionless[rad],
which contains an incorrect unit for a
dimensionless quantity. Such a
conversion must be explicit and be preceded by an explicit unit
conversion:
quantity q1 = isq::angular_measure(42. * rad);
quantity<dimensionless[one]> q2 = dimensionless(q1.in(one));Truncation prevention and Unit safety chapters describe the motivation,
usage, and safety benefits of the
value_cast,
in, and
force_in value conversion
functions.
template
disambiguation concernsInitially mp-units library allowed changing of the
quantity representation type only
via the value_cast non-member
function. Introducing such a functionality to
in and
force_in member functions would
mandate the usage of the
template
disambiguator in generic contexts that we encorage with Generic interfaces.
After bringing those concerns to LEWGI in St. Louis, the room agreed
that we should provide this functionality for member functions as well.
It is really useful and user-friendly in non-generic contexts and for
the cases where we deal with a dependent name, we should leave
value_cast even if it is an always
conversion-forcing operation.
The table below provides all the value conversions functions 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) |
force_in(U)force_in is a bit ambiguous name
for the conversion function in a quantities and units library. Writing
x.force_in(s)
may be misleading for a quantity of time rather than
force. However, we do not have good alternatives here.
Before we provide some alternatives it is good to mention that we
also heve a x.force_numerical_value_in(u)
to force a truncation while obtaining a numerical value of the
quantity.
[Au] library uses x.coerce_in(u)
for this operation. We could also consider different names. Here are a
few possbile alternatives:
x.force_in(u),
x.force_numerical_value_in(u),x.forced_into(u),
x.forced_numerical_value_into(u),x.unsafe_in(u),
x.unsafe_numerical_value_in(u),x.lossy_in(u),
x.lossy_numerical_value_in(u),x.unchecked_in(u),
x.unchecked_numerical_value_in(u),x.coerce_in(u),
x.coerce_numerical_value_in(u),x.cast_in(u),
x.cast_numerical_value_in(u),x.cast_to(u),
x.cast_numerical_value_to(u).In case we select x.cast_to(u)
we probably should also rename q.in(u)
to q.to(u).
quantity::rep[mp-units] initially tried to be
compatible with std::chrono::duration.
This is why we chose rep as the name
for a public member type exposed from
quantity to denote its
representation type. This is consistent but may not be the best
name.
First, we use q.numerical_value_in()
to get the underlying value which is already inconsistent with std::chrono::duration::count().
Also, as we mentioned already, quantity
is a numeric wrapper. To provide compatibility between different
numeric types maybe we should set a policy that those should expose
value_type or
element_type? Both seem to be valid
choices here as well.
Binary operators for quantities (and quantity points) should take both arguments as template parameters. Implementing them in terms of implicit convertibility leads to invalid resulting types. Let’s see the following example:
static_assert(std::convertible_to<quantity<isq::speed[m/s], int>,
quantity<(isq::length / isq::time)[m/s], double>>);
static_assert(!std::convertible_to<quantity<(isq::length / isq::time)[m/s], double>,
quantity<isq::speed[m/s], int>>);As we see above, quantity<isq::speed[m/s], int>
converts to quantity<(isq::length / isq::time)[m/s], double>,
but this is not the case in the other direction. This is caused by the
fact that conversion from
double to
int is
considered truncating. If we would implement the operators in terms of
the implicit conversion then we would end up with the quantity of isq::length / isq::time
as a result, which is suboptimal. We prefer
isq::speed
in this case:
quantity q1 = isq::speed(1 * m/s);
quantity q2 = isq::length(1. * m) / isq::time(1. * s);
static_assert(is_of_type<q1 + q2, quantity<isq::speed[m/s], double>>);In the following example, we consistently use floating-point representation types and both quantities are interconvertible:
static_assert(std::convertible_to<quantity<(isq::mass * pow<2>(isq::length / isq::time))[J], double>,
quantity<isq::energy[kg*m2/s2], double>>);
static_assert(std::convertible_to<quantity<isq::energy[kg*m2/s2], double>,
quantity<(isq::mass * pow<2>(isq::length / isq::time))[J], double>>);We could think that the problem is gone, and we can use implicit
conversions. However, depending on how we implement it, this might lead
to an ambiguous overload resolution or lack of substitutability of
addition. Even if we somehow solve those issues, none of the types would
be perfect as a return type. While forming a resulting type
isq::energy
should have a priority over isq::mass * pow<2>(isq::length / isq::time)
and J should have a priority over
kg*m2/s2:
quantity q1 = (isq::mass(1 * kg) * pow<2>(isq::length(1 * m) / isq::time(1 * s))).in(J);
quantity q2 = isq::energy(1 * kg*m2/s2);
static_assert(is_of_type<q1 + q2, quantity<isq::energy[J], int>>);It is also worth noting that this approach is compatible with binary operators
for std::chrono::duration.
As we can read in the Interoperability
with the
std::chrono
abstractions chapter, std::chrono::duration
is interconvertible with the quantity. Nevertheless, with the above, we
always need to explicitly convert the argument to a proper entity before
doing any arithmetic:
static_assert(1 * s + 1s == 2 * s); // does not compile
static_assert(1 * s + quantity{1s} == 2 * s); // OKThis prevents ambiguity with std::chrono::duration
operators and works the same for any user-defined
QuantityLike type or any other type
that is convertible to a
quantity.
delta and
point creation helpersThe 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:
quantity and
quantity_point with the
delta and
point construction helpers
respectively.Here are the main points of this new design:
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.
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>.A new point quantity point
construction helper is introduced:
point<m>(42)
results with a quantity_point<si::metre, zeroth_point_origin<kind_of<isq::length>>{}, int>,point<deg_C>(5)
results with a quantity<si::metre, si::ice_point, int>.Please note that
si::kelvin
is also excluded from the multiply syntax to prevent the following
surprising issues:
Before
|
Now
|
|---|---|
|
|
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 = point<deg_C>(28.0);
auto Temperature = point<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.
default_point_origin<Reference>,
quantity_from_unit_zero(),
and natural_point_origin<QuantitySpec>default_point_origin<Reference>,
quantity_from_unit_zero(),
and natural_point_origin<QuantitySpec>
are introduced to simplify the usage of:
In theory, those abstractions are not needed, and in this chapter, we will describe how the API and use cases would look like without it.
Let’s try to reimplement parts of our room AC temperature controller from the Temperature support chapter:
constexpr struct room_reference_temp : relative_point_origin<si::zeroth_degree_Celsius + delta<deg_C>(21)> {} room_reference_temp;
using room_temp = quantity_point<isq::Celsius_temperature[deg_C], room_reference_temp>;
room_temp room_ref{};
std::println("Room reference temperature: {} ({}, {::N[.2f]})\n",
room_ref.in(deg_C).quantity_from(si::zeroth_degree_Celsius),
room_ref.in(deg_F).quantity_from(usc::zeroth_degree_Fahrenheit),
room_ref.in(K).quantity_from(si::zeroth_kelvin));Now let’s compare it to the implementation using a currently proposed design:
constexpr struct room_reference_temp : relative_point_origin<point<deg_C>(21)> {} room_reference_temp;
using room_temp = quantity_point<isq::Celsius_temperature[deg_C], room_reference_temp>;
room_temp room_ref{};
std::println("Room reference temperature: {} ({}, {::N[.2f]})\n",
room_ref.quantity_from_unit_zero(),
room_ref.in(deg_F).quantity_from_unit_zero(),
room_ref.in(K).quantity_from_unit_zero());First, removing those features also renders point<deg_C>(21)
impossible to implement. Second, mandating an explicit point origin when
we convert to quantity makes the
code harder to maintain as we have to track a current unit of a quantity
carefully. If someone changes a unit, a point origin also has to be
changed to get meaningful results. This is why, to ensure that we are
origin-safe, we also need to provide .in(deg_C).
in the first print argument.
In the proposed design, the above problems are eliminated with the
quantity_from_unit_zero()
usage that always returns a proper value for a current unit.
It is 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:
quantity_like_traits for
external quantity-like type,quantity_point_like_traits for
external quantity_point-like
type.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 abstractions in this library) 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, in the C++ language, the implicit conversions are allowed in cases where:
In all other scenarios, we should probably enforce explicit conversions.
The kinds of inter-library conversions can be easily configured in
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_import to describe
conversion from the external entity to the one in this library (import
case),explicit_export to describe
conversion from the entity in this library to the external one (export
case).For example, let’s assume that some company has its own
Meter strong-type wrapper:
struct Meter {
int value;
};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 std::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 specialization of
the quantity_like_traits<T>
type trait. Such specialization should provide:
reference static data member
that provides the quantity reference (e.g., unit),rep type that specifies the
underlying storage type,explicit_import static data
member convertible to
bool that
specifies that the conversion from T
to a quantity type should happen
explicitly (if
true),explicit_export static data
member convertible to
bool that
specifies that the conversion from a
quantity type to
T should happen explicitly (if
true),to_numerical_value(T)
static member function returning a quantity’s raw value of
rep type,from_numerical_value(rep)
static member function returning
T.For example, for our Meter type,
we could provide the following:
template<>
struct std::quantity_like_traits<Meter> {
static constexpr auto reference = si::metre;
static constexpr bool explicit_import = false;
static constexpr bool explicit_export = false;
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:
static_assert(QuantityLike<Meter>);and we can write the following:
void print(Meter m) { std::cout << m.value << " m\n"; }
int main()
{
using namespace std::si::unit_symbols;
Meter height{42};
// implicit conversions
std::quantity h1 = height;
std::quantity<isq::height[m], int> h2 = height;
std::cout << h1 << "\n";
std::cout << h2 << "\n";
// explicit conversions
print(Meter(h1));
print(Meter(h2));
}No matter if we decide to use implicit or explicit conversions, the library’s framework 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:
Unsafe
|
Fixed
|
|---|---|
|
|
(1)
Truncation of value while converting from meters to kilometers.
(2)
Conversion of
double to
int is not
value-preserving.
(3)
Truncation of value while converting from millimeters to meters.
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 this library to work with
this abstraction.
struct Timestamp {
int seconds;
};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 specialization of the quantity_point_like_traits<T>
type trait:
reference static data member
that provides the quantity point reference (e.g., unit),point_origin static data member
that specifies the absolute point, which is the beginning of our
measurement scale for our points,rep type that specifies the
underlying storage type,explicit_import static data
member convertible to
bool that
specifies that the conversion from T
to a quantity type should happen
explicitly (if
true),explicit_export static data
member convertible to
bool that
specifies that the conversion from a
quantity type to
T should happen explicitly (if
true),to_numerical_value(T)
static member function returning a raw value of the
quantity being the offset of the
point from the origin,from_numerical_value(rep)
static member function returning
T.For example, for our Timestamp
type, we could provide the following:
template<>
struct std::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 = false;
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:
static_assert(std::QuantityPointLike<Timestamp>);and we can write the following:
void print(Timestamp ts) { std::cout << ts.seconds << " s\n"; }
int main()
{
Timestamp ts{42};
// implicit conversion
std::quantity_point qp = ts;
std::cout << qp.quantity_from_unit_zero() << "\n";
// explicit conversion
print(Timestamp(qp));
}std::chrono
abstractionsIn 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.This library comes with built-in interoperability with those types thanks to:
quantity_like_traits and
quantity_point_like_traits that
provide support for implicit conversions between types in both
directions,chrono_point_origin<Clock>
point origin for std clocks,to_chrono_duration and
to_chrono_time_point dedicated
conversion functions that result in types exactly representing this
library’s abstractions.It is important to note here that 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 : relative_point_origin<chrono_point_origin<system_clock> + 1 * h> {} ts_origin;
inline constexpr struct my_origin : 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)(1)
my_origin is not defined in terms of
chrono_point_origin<Clock>.
(2)
natural_point_origin is not defined
in terms of chrono_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:
Takeoff: 2023-11-18 13:20:54 UTC
Landing: 2023-11-18 15:07:01 MSTAs mentioned above, conversions between entities in this and
std::chrono
libraries are implicit in both directions. This simplifies many
scenarios. However, with such rules, common_type_t<chrono::seconds, quantity<si::second, int>>;
and the ternary operator on such arguments will not work. If this
concerns LEWG, we may consider implicit conversion in only one
direction. However, it is not easy to decide which one to choose.
Through the last years [mp-units] library proved to be very intuitive to both novices in the domain and non-C++ experts. Thanks to the user-friendly multiply syntax, support for CTAD, excellent readability of generated types in compiler error messages, and simplicity of systems definitions, this library makes it easy to do the first steps in the dimensional analysis domain.
Following the practice suggested in [P1700R0], we identify four distinct user populations for this library, each with different needs and interactions:
Audience
|
Population
|
Roles and skills
|
|---|---|---|
| Application Developers | millions | Write application code using pre-defined quantities, units, and
systems. Perform arithmetic with automatic dimensional analysis and
compile-time unit safety. Use
quantity for deltas and
quantity_point for points and
measurements (temperature, GPS, timestamps). Write generic interfaces
constrained with QuantityOf.
Interoperate with
std::chrono.
Modernise existing codebases by replacing raw numeric types with
strongly-typed quantities. |
| Unit Authors | tens of thousands | Add named or scaled units to existing quantity types — standard-system extensions (imperial, binary prefixes) and derived combinations. Work entirely within the provided ISQ hierarchy; no new dimensions or quantity specifications required. |
| Domain Modelers | thousands | Properly model a new domain: define quantity systems (ISQ-like hierarchies), dimensions, quantity specifications, quantity kind hierarchies, and units. Design domain frameworks (physics engines, geodesy libraries, robotics toolkits) with correct ISQ-style structure. Require domain knowledge and familiarity with the quantity type system; deep C++ metaprogramming is not needed. |
| Deep Integrators | hundreds | Bridge custom or legacy types into the quantity system via
quantity_like_traits. Implement
custom representation types with specialised scaling behaviour. Require
template metaprogramming expertise and understanding of library
internals; domain-specific quantity modelling is not needed. |
This clear separation ensures that the vast majority of users (Application Developers) can be productive immediately with minimal learning, while still providing extensibility for expert users.
The following table maps library features to their primary target audiences:
Feature
|
Application Developers
|
Unit Authors
|
Domain Modelers
|
Deep Integrators
|
|---|---|---|---|---|
Multiply syntax (42 * m) |
✓ | ✓ | ✓ | ✓ |
| CTAD for quantities | ✓ | ✓ | ✓ | ✓ |
Arithmetic operations
(+,
-,
*,
/) |
✓ | ✓ | ✓ | ✓ |
Unit conversions (.in(unit)) |
✓ | ✓ | ✓ | ✓ |
| Comparison operators | ✓ | ✓ | ✓ | ✓ |
Standard unit symbols
(si::metre,
usc::foot) |
✓ | ✓ | ✓ | ✓ |
Text formatting with
std::format |
✓ | ✓ | ✓ | ✓ |
Extracting numerical values (.numerical_value_in()) |
✓ | ✓ | ✓ | ✓ |
std::chrono
interop |
✓ | ✓ | ✓ | ✓ |
quantity vs
quantity_point (basic usage) |
✓ | ✓ | ✓ | ✓ |
Generic interfaces (QuantityOf<isq::length>) |
✓ | ✓ | ✓ | ✓ |
Defining custom units
(named_unit) |
✓ | ✓ | ✓ | |
| Unit prefixes (SI and binary) | ✓ | ✓ | ✓ | |
Scaled units (mag<N> * unit,
mag_constant) |
✓ | ✓ | ✓ | |
| Systems of quantities (ISQ-like hierarchies) | ✓ | |||
Defining quantity types
(quantity_spec) |
✓ | |||
Custom dimensions
(derived_dimension) |
✓ | |||
Quantity kind hierarchies
(is_kind) |
✓ | |||
| Custom point origins | ✓ | |||
Representation type constraints
(RepresentationOf) |
✓ | ✓ | ||
| Symbolic expression templates | ✓ | |||
Magnitude framework
(mag_power) |
✓ | |||
Custom quantity_like_traits |
✓ | |||
| Custom representation types | ✓ |
Application Developers need only the first eleven rows — the core usage features. Unit Authors additionally define named and scaled units within the existing ISQ hierarchy. Domain Modelers and Deep Integrators are largely disjoint audiences: Domain Modelers bring domain expertise to define quantity systems, dimensions, and specifications, while Deep Integrators bring C++ metaprogramming expertise to extend the library’s type machinery. Both groups build on Unit Author skills, but neither needs the other’s specialisation.
Students should have basic familiarity with:
The library is suitable for:
No prior knowledge of template metaprogramming or dimensional analysis is required for basic usage.
Starting with compelling examples helps students understand why strong typing matters:
These examples demonstrate that unit errors are costly, hard to spot in code review, and can slip through testing. A quick demonstration of untyped vs. typed code makes the value proposition clear:
// Unsafe - compiles but crashes spacecraft
double orbital_velocity(double radius, double period)
{
return 2 * 3.14159 * radius / period;
}
auto v = orbital_velocity(400000, 5400); // What units? Which one is length? Compiler can't tell!
// Safe - units enforced at compile time
quantity<si::metre / si::second> orbital_velocity(quantity<si::metre> auto radius,
quantity<si::second> auto period)
{
return 2 * pi * radius / period;
}
quantity v = orbital_velocity(400 * km, 90 * min); // OK: 279 m/sIf someone is new to the domain, a concise introduction of Systems of units, the [SI], and the US Customary System (and how it relates to SI) might be needed.
After that, every new user, even a C++ newbie, should have no problems with understanding the topics of the Quantity construction chapter and should be able to start using the library successfully. At least as long as they keep operating in the safety zone using floating-point representation types.
Start with the multiply syntax for creating quantities:
import std;
int main()
{
using namespace std::si::unit_symbols;
quantity distance = 100.0 * m;
quantity time = 9.58 * s;
quantity speed = distance / time;
std::println("Usain Bolt's speed: {::N[.2f]}", speed); // 10.44 m/s
}Key teaching points:
100.0 * m
reads like physics notationm / s
derived from divisiondistance + timeEventually, the library will stand in the way, disallowing “unsafe” conversions. This would be a perfect place to mention the importance of providing safe interfaces at compile-time and describe why narrowing conversions are unwelcome and what the side effects of those might be. After that, forced conversions in the library should be presented.
Demonstrate safe conversions:
quantity<m> race_distance = 100. * m;
quantity<km> trip_distance = race_distance.in(km); // Safe: 0.1 km
quantity<m, int> d1 = 5 * m; // OK
quantity<m, int> d2 = 5.5 * m; // Error: narrowing conversion
quantity<m, int> d3 = (5.5 * m).force_in<int>(); // Explicit truncation
quantity<km, int> d4 = 1500 * m; // Error: truncation in conversion
quantity<km, int> d5 = (1500 * m).force_in(km); // Explicit: d5 == 1 kmThis naturally introduces:
.in(unit)
member function for safe conversions.force_in()
for intentional lossy conversionsIn case a target audience needs to interact with legacy interfaces that take raw numeric values, Safe quantity numerical value getters chapter should be introduced. In such a case, it is important to warn students of why this operation is unsafe and what are the potential maintainability issues.
// Legacy API
void legacy_api(double distance_in_meters);
// Modern code
quantity dist = 5 * km;
legacy_api(dist.numerical_value_in(m)); // Explicit: I know this is in metersEmphasize the dangers: the compiler cannot verify that
distance_in_meters actually expects
meters rather than feet or kilometers. You must manually ensure the unit
in .numerical_value_in()
matches the legacy API’s expectations, which may be undocumented or
ambiguous.
Next, we could show how easy extending the library with custom units is. A simple and funny example like the below could be a great exercise here:
import std;
inline constexpr struct smoot : std::named_unit<"smoot", std::mag<67> * std::usc::inch> {} smoot;
int main()
{
constexpr std::quantity dist = 364.4 * smoot;
std::println("Harvard Bridge length = {::N[.5]} ({::N[.5]}, {::N[.5]}) ± 1 εar",
dist, dist.in(std::usc::foot), dist.in(std::si::metre));
}This demonstrates that the library is extensible and students can define domain-specific units (e.g., furlongs per fortnight for astronomy, pixels for graphics).
After a while, we can also introduce students to The affine space abstractions and discuss the Temperature support.
This introduces the distinction between differences
(quantity) and absolute points
(quantity_point), using temperature
as the most intuitive example:
quantity temp_diff = 20 * delta<deg_C>; // Temperature difference
quantity_point temp = 20 * point<deg_C>; // Absolute temperature
quantity diff = temp - point<deg_C>(0); // OK: difference between points
// auto sum = temp + point<deg_C>(10); // Error: can't add pointsWith the above, we have learned enough for most users’ needs and do not need to delve into more details. The library is intuitive and will prevent all errors at compile time.
For more advanced classes, groups, or use cases, we can introduce Generic Interfaces and Systems of quantities but we don’t have to describe every detail and corner cases of quantity types design and their convertibility. It is good to start here with Why do we need typed quantities?, followed by Quantities of the same kind and System of quantities is not only about kinds.
According to our experience, the most common pitfall in using quantity types might be related to the names chosen for them by the [ISO/IEC 80000] (e.g., length). It might be good to describe what length means when we say “every height is a length” and what it means when we describe a box of length, width, and height dimensions. In the latter case, length will not restrict us to the horizontal dimension only. This is how the ISQ is defined, and we should accept this. However, we should present a way to define horizontal length as presented in the Comparing, adding, and subtracting quantities of the same kind and describe its rationale.
Advanced students can explore:
QuantityOf<isq::length>)horizontal_length,
gravitational_potential_energy)std::chrono::duration
and other librariesOne of the library’s strongest teaching features is compiler error quality. When students make mistakes, they get readable messages:
quantity d = 100 * m;
quantity t = 50 * s;
quantity wrong = d + t; // ErrorType names in errors remain close to the source code: quantity<si::metre, double>
rather than pages of template instantiation noise.
Debugging is similarly friendly: quantity objects display naturally
in debuggers showing both value and unit. Print formatting with
std::print
produces human-readable output without custom formatters.
Suggested exercises for different skill levels:
Beginner:
Intermediate:
Advanced:
quantity_like_traitsBased on teaching experience with [mp-units]:
quantity d = 1500 * m; quantity km_val = d.in(km);
fails (truncation)quantity<nm, int> small = 5 * m;
fails (overflow on scaling factor).force_in()
when integer truncation is intentionalquantity frequency = 1 / 2 * s;
gives 0.5 s,
not
0.5 Hzquantity frequency = 1 / (2 * s);
or quantity period = 2 * s; auto frequency = 1 / period;quantity and
quantity_point: Attempting
invalid affine space operations
auto result = 20 * deg_C + 30 * deg_C;
- can’t add absolute temperatures42 * m
without importing unit symbols
using namespace std::si::unit_symbols;
or use qualified names.numerical_value_in():
Extracting raw values unnecessarily
This library fits naturally into existing courses:
CS1/CS2 (Introductory Programming):
Numerical Methods / Scientific Computing:
double
arrays with typed quantitiesSoftware Engineering:
Physics / Engineering Computation:
While the previous sections focus on teaching the library itself, this feature has broader pedagogical value: it transforms C++ into a teaching tool for units and quantities in general. A standardized quantities and units library extends C++’s educational reach beyond traditional computer science into physics, engineering, and even primary education.
The library provides non-CS educators with a robust computational tool for teaching their subject matter. Physics teachers can build hands-on computational exercises where students implement real physics equations (projectile motion, circuit analysis, thermodynamics) with compile-time verification that their dimensional analysis is correct. Engineering instructors can create laboratory exercises where sensor data is processed with proper unit handling from the start, mirroring professional practice. Chemistry educators can teach stoichiometry and gas laws with students writing code that enforces correct unit conversions between moles, grams, liters, and atmospheres.
This creates opportunities for integrated learning where domain
knowledge and computational thinking develop together. A student
learning physics doesn’t just memorize formulas—they implement them, and
the compiler verifies their understanding of dimensional relationships.
When a student writes force = mass * acceleration
and the library confirms the result has units of force, they’ve
demonstrated conceptual understanding in a way that traditional problem
sets cannot assess.
Many science and engineering faculty have computational needs but
limited software engineering expertise. The library’s intuitive multiply
syntax (distance = 50 * km)
requires minimal C++ knowledge while providing significant safety
benefits. Faculty can create course materials using straightforward code
that reads like mathematical notation, lowering the barrier to
incorporating computation into their curriculum. The library’s
compile-time error messages serve double duty: they catch programming
mistakes and reveal dimensional analysis errors that indicate
conceptual misunderstandings.
This is particularly valuable for disciplines where programming is auxiliary to the primary subject. An engineering professor teaching fluid mechanics doesn’t need to become a C++ expert—basic quantities and units operations are accessible with minimal training while still providing the benefits of type safety and dimensional analysis.
Computing education increasingly begins in primary and secondary
schools. Robotics programs, often built around platforms like LEGO
Mindstorms or Arduino, provide rich opportunities to introduce
quantities and units naturally. A middle school robotics team
programming their robot to navigate a course benefits from using speed = 30 * cm / s
instead of raw numbers. The explicit units make the code
self-documenting for young programmers still building intuition about
physical quantities.
When students can write if (distance < 50 * cm) { stop(); }
the connection between their physical robot and their code becomes
clearer. The compiler preventing them from comparing distance to time
reinforces dimensional understanding at an age when these concepts are
still forming. This creates a richer learning environment where
programming and physical intuition develop in parallel.
Similarly, informal education settings—science museums, summer camps, maker spaces—can leverage the library in interactive exhibits and workshops. The combination of physical computing (sensors, actuators) with properly-typed quantities provides immediate, tangible feedback about both programming and physics concepts.
Standardization ensures students learn an industry-relevant skill.
Unlike toy educational languages or frameworks that must be unlearned
later, proper units handling in C++ directly transfers to professional
software development. Students who learn to write quantity area = width * height;
where width and
height are quantities develop habits
that prevent real-world errors in safety-critical systems.
This is particularly important for students entering industries where C++ dominates: aerospace, automotive, embedded systems, robotics, quantitative finance, and game development. A graduate who has used quantities and units throughout their coursework arrives at their first job already familiar with dimensional analysis in code, ready to contribute safely to production systems.
The standardization aspect is crucial here—without it, each company uses incompatible internal libraries or ad-hoc approaches, forcing new graduates to relearn concepts they should already know. A standard library provides a stable foundation that educational institutions can confidently build curricula around, knowing their graduates will use these same tools professionally.
Special thanks and recognition goes to The C++ Alliance for supporting Mateusz’s membership in the ISO C++ Committee and the production of this proposal.
We would also like to thank:
Users should not select unit_symbol_separator::half_high_dot
and character_set::portable
at the same time. This symbol is valid only for UTF-8 encoding.
Otherwise, we propose to throw an exception during the unit symbol
string processing. We could also ignore the unit_symbol_separator::half_high_dot
option and use space ” “, like this is the case for character_set::portable.↩︎
For integral reps, conversion factors are always integers. Non-integer factors already fail due to truncation, so overflow is moot.↩︎