1. Revision History
1.1. r1 ➡ r2
-
[MP-UNITS] code samples updated to reflect the current library design
-
§ 10 Other design decisions chapter added
-
§ 10.1 Systems support chapter reworked
-
§ 10.2 Dimensionless quantities chapter reworked
-
§ 10.3 No runtime-specified conversions chapter added
-
§ 10.4 No initial support for quantity kinds chapter added
-
§ 10.5 Text output chapter added
-
§ 14 Open questions chapter reworked
1.2. r0 ➡ r1
-
New definitions added (coherent system of units, base dimension, derived dimension, reduced dimension) to § 4 Terms and definitions
-
§ 5 Prior Work chapter updated
-
Prefixes and units chapter extended with last paragraph
-
§ 10.1 Systems support extended
-
Integral UDLs chapter updated
-
Number concept chapter added
-
Unicode chapter added
-
§ 16 Implementation Experience extended
-
§ 14.3 Interoperability with std::chrono::duration alternative 2 replaced with a better idea
-
§ 17 Polls extended and split to separate ISO C++ rooms
-
[MP-UNITS] code samples updated to reflect the current library design
2. Introduction
2.1. Overview
Human history knows many expensive failures and accidents caused by mistakes in calculations involving different physical units. 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]. That is not the only example here. People tend to confuse units quite often. We see similar errors occurring in various domains over the years:
-
On October 12, 1492, Christopher Columbus unintentionally discovered America because during his travel preparations he mixed Arabic mile with a Roman mile which led to the wrong estimation of the equator and his expected travel distance [COLUMBUS]
-
Air Canada Flight 143 ran out of fuel on July 23, 1983, at an altitude of 41 000 feet (12 000 metres), midway through the flight because the fuel had been calculated in pounds instead of kilograms by the ground crew [GIMLI_GLIDER]
-
On April 15, 1999, Korean Air Cargo Flight 6316 crashed due to the miscommunication between pilots about desired flight altitude [FLIGHT_6316]
-
In February 2001 Zoo crew built an enclosure for Clarence the Tortoise with a weight of 250 pounds instead of 250 kilograms [CLARENCE]
-
In December 2003, one of the roller coaster’s cars at Tokyo Disneyland’s Space Mountain attraction suddenly derailed due to a broken axle caused by the confusion after upgrading the specification from imperial to metric units [DISNEY]
-
An American company sold a shipment of wild rice to a Japanese customer, quoting a price of 39 cents per pound, but the customer thought the quote was for 39 cents per kilogram [WILD_RICE]
-
A whole set of medication dose errors...
2.2. Lack of strong types
It turns out that in the C++ software most of our calculations in the physical units domain
are handled with fundamental types like
. Code like below is a typical example
here:
double GlidePolar :: MacCreadyAltitude ( double emcready , double Distance , const double Bearing , const double WindSpeed , const double WindBearing , double * BestCruiseTrack , double * VMacCready , const bool isFinalGlide , double * TimeToGo , const double AltitudeAboveTarget , const double cruise_efficiency , const double TaskAltDiff );
Even though this example comes from an Open Source project, expensive revenue-generating production source code often does not differ too much. We lack strong typedefs feature in the core language, and without it, we are often too lazy to handcraft a new class type for each use case.
2.3. The proliferation of magic numbers
There are a lot of constants and conversion factors involved in the dimensional analysis. Source code responsible for such computations is often trashed with magic numbers
// Air Density(kg/m3) from relative humidity(%), // temperature(°C) and absolute pressure(Pa) 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 ))); }
3. Motivation and Scope
3.1. Motivation
There is a huge demand for high-quality physical units library in the industry and scientific environments. The code that we write for fun and living should be correct, safe, and easy to write. Although there are multiple such libraries available on the market, none of them is a widely accepted production standard. We could just provide a yet another 3rd party library covering this topic, but it is probably not the best idea.
First of all, software that could benefit from such a library is not a niche in the market.
If it was the case, probably its needs could be fulfilled with a 3rd party highly-specialized
and narrow-use library. On the contrary, a broad range of production projects deals with units
conversions and dimensional analysis. Right now, having no other reasonable and easy to access
alternatives results in the proliferation of plain
type usage to express physical
quantities. Space, aviation, automotive, embedded, scientific, computer science, and many
other domains could benefit from strong types and conversions provided by such a library.
Secondly, yet another library will not solve the issue for many customers. Many corporations
are not allowed to use 3rd party libraries in the production code. Also, an important point
here is the cooperation of different products from multiple vendors that use physical quantities
as vocabulary types in their interfaces. From the author’s experience gathered while working
with numerous corporations all over the world, there is a considerable difference between the
adoption of a mature 3rd party library and the usage of features released as a part of
the C++ Standard Library. If it were not the case all products would use Boost.Units already.
A motivating example here can be
released as a part of C++11. Right now, no one
asks questions on how to represent timestamps and how to handle their conversions in the code.
is the ultimate answer. So let us try to get
in the C++
Standard Library too.
3.2. The Goal
The aim of this paper is to standardize a physical units library that enables operations on various dimensions and units:
// simple numeric operations static_assert ( 10 km / 2 == 5 km ); // unit conversions static_assert ( 1 h == 3600 s ); static_assert ( 1 km + 1 m == 1001 m ); // dimension conversions static_assert ( 1 km / 1 s == 1000 mps ); static_assert ( 2 kmph * 2 h == 4 km ); static_assert ( 2 km / 2 kmph == 1 h ); static_assert ( 1000 / 1 s == 1 kHz ); static_assert ( 10 km / 5 km == 2 );
We intent to provide users with cleaner interfaces by using strong types and concepts in the interfaces rather than fundamental types with meaning described in comments or documentation:
constexpr std :: units :: Velocity auto avg_speed ( std :: units :: Length auto d , std :: units :: Time auto t ) { return d / t ; }
We further aim to provide unit conversion facilities and constants for users to rely on, instead of magic numbers:
using namespace std :: units_literals ; const std :: units :: Velocity auto speed = avg_speed ( 220 km , 2 h ); std :: cout << "Average speed: " << std :: units :: quantity_cast < std :: units :: si :: kilometre_per_hour > ( speed ) << '\n' ;
3.3. Scope
There is a public demand for a generic units library that could handle any units and dimensions. There are many, often conflicting, requirements. Some of them can be found in [P1930R0].
To limit the initial scope, the author suggests scoping the Committee efforts only on the
physical and possibly computer science (i.e.
,
,
) units first. The library
should be designed with easy extensibility in mind so anyone needing a new base or derived
dimensions (i.e.
system) could achieve this with a few lines of
the C++ code (not preprocessor macros).
After releasing a first, restricted version of the library and observing how it is used we can consider standardizing additional dimensions, units, and constants in the following C++ releases.
4. Terms and definitions
4.1. ISO 80000-1:2009(E) definitions
ISO 80000-1:2009(E) Quantities and units - Part 1: General [ISO_80000-1] defines among others the following terms:
quantity
-
Property of a phenomenon, body, or substance, where the property has a magnitude that can be expressed by means of a number and a reference.
-
A reference can be a measurement unit, a measurement procedure, a reference material, or a combination of such.
-
A quantity as defined here is a scalar. However, a vector or a tensor, the components of which are quantities, is also considered to be a quantity.
-
The concept ’quantity’ may be generically divided into, e.g. ‘physical quantity’, ‘chemical quantity’, and ‘biological quantity’, or ‘base quantity’ and ‘derived quantity’.
-
Examples of quantities are: mass, length, density, magnetic field strength, etc.
kind of quantity, kind
-
Aspect common to mutually comparable quantities.
-
The division of the concept ‘quantity’ into several kinds is to some extent arbitrary
-
i.e. the quantities diameter, circumference, and wavelength are generally considered to be quantities of the same kind, namely, of the kind of quantity called length.)
-
-
Quantities of the same kind within a given system of quantities have the same quantity dimension. However, quantities of the same dimension are not necessarily of the same kind.
-
For example, the absorbed dose and the dose equivalent have the same dimension. However, the former measures the absolute amount of radiation one receives whereas the latter is a weighted measurement taking into account the kind of radiation on was exposed to.
-
system of quantities, system
-
Set of quantities together with a set of non-contradictory equations relating those quantities.
-
Examples of systems of quantities are: the International System of Quantities, the Imperial System, etc.
base quantity
-
Quantity in a conventionally chosen subset of a given system of quantities, where no quantity in the subset can be expressed in terms of the other quantities within that subset.
-
Base quantities are referred to as being mutually independent since a base quantity cannot be expressed as a product of powers of the other base quantities.
derived quantity
-
Quantity, in a system of quantities, defined in terms of the base quantities of that system.
International System of Quantities (ISQ)
-
System of quantities based on the seven base quantities: length, mass, time, electric current, thermodynamic temperature, amount of substance, and luminous intensity.
-
The International System of Units (SI) is based on the ISQ.
dimension of a quantity, quantity dimension, dimension
-
Expression of the dependence of a quantity on the base quantities of a system of quantities as a product of powers of factors corresponding to the base quantities, omitting any numerical factors.
-
A power of a factor is the factor raised to an exponent. Each factor is the dimension of a base quantity.
-
In deriving the dimension of a quantity, no account is taken of its scalar, vector, or tensor character.
-
In a given system of quantities:
-
quantities of the same kind have the same quantity dimension,
-
quantities of different quantity dimensions are always of different kinds,
-
quantities having the same quantity dimension are not necessarily of the same kind.
-
quantity of dimension one, dimensionless quantity
-
Quantity for which all the exponents of the factors corresponding to the base quantities in its quantity dimension are zero.
-
The term “dimensionless quantity” is commonly used and is kept here for historical reasons. It stems from the fact that all exponents are zero in the symbolic representation of the dimension for such quantities. The term “quantity of dimension one” reflects the convention in which the symbolic representation of the dimension for such quantities is the symbol 1. This dimension is not a number, but the neutral element for multiplication of dimensions.
-
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. The coherent derived unit is the number one, symbol 1.
-
Numbers of entities are quantities of dimension one.
unit of measurement, measurement unit, unit
-
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 second quantity to the first one as a number.
-
Measurement units are designated by conventionally assigned names and symbols.
-
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).
-
Measurement units of quantities of dimension one are numbers. In some cases, these measurement units are given special names, e.g. radian, steradian, and decibel, or are expressed by quotients such as millimole per mole equal to 10−3 and microgram per kilogram equal to 10−9.
base unit
-
Measurement unit that is adopted by convention for a base quantity.
-
In each coherent system of units, there is only one base unit for each base quantity.
-
A base unit may also serve for a derived quantity of the same quantity dimension.
-
For example, the ISQ has the base units of: metre, kilogram, second, Ampere, Kelvin, mole, and candela.
derived unit
-
Measurement unit for a derived quantity.
-
For example, in the ISQ Newton, Pascal, and katal are derived units.
coherent derived unit
-
Derived unit that, for a given system of quantities and for a chosen set of base units, is a product of powers of base units with no other proportionality factor than one.
-
A power of a base unit is the base unit raised to an exponent.
-
Coherence can be determined only with respect to a particular system of quantities and a given set of base units. That is, if the metre and the second are base units, the metre per second is the coherent derived unit of velocity.
system of units
-
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.
coherent system of units
-
System of units, based on a given system of quantities, in which the measurement unit for each derived quantity is a coherent derived unit.
-
A system of units can be coherent only with respect to a system of quantities and the adopted base units.
off-system measurement unit, off-system unit
-
Measurement unit that does not belong to a given system of units. For example, the electronvolt (≈ 1,602 18 × 10–19 J) is an off-system measurement unit of energy with respect to the SI or day, hour, minute are off-system measurement units of time with respect to the SI.
International System of Units (SI)
-
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)
multiple of a unit
-
Measurement unit obtained by multiplying a given measurement unit by an integer greater than one.
-
SI prefixes refer strictly to powers of 10, and should not be used for powers of 2. That is, 1 kbit should not be used to represent 1024 bits (210 bits), which is a kibibit (1 Kibit).
submultiple of a unit
-
Measurement unit obtained by dividing a given measurement unit by an integer greater than one.
quantity value, value of a quantity, value
-
Number and reference together expressing magnitude of a quantity.
-
A quantity value can be presented in more than one way.
4.2. Other definitions
base dimension
-
A dimension of a base quantity.
derived dimension
-
A dimension of a derived quantity.
-
Often implemented as a list of exponents of base dimensions.
normalized dimension
A derived dimension in which:
-
base dimensions are not repeated in a list (each base dimension is provided at most once),
-
base dimensions are consistently ordered,
-
base dimensions having zero exponent are elided.
5. Prior Work
There are multiple dimensional analysis libraries available on the market today. Some of them are more successful than others, but none of them is a widely accepted standard in the C++ codebase (both for Open Source as well as production code). The next sections of this chapter will describe the most interesting parts of selected libraries. The last section provides an extensive comparison of their main features.
5.1. Boost.Units
Boost.Units [BOOST.UNITS] is probably the most widely adopted library in this domain. It was first included in Boost 1.36.0 that was released in 2008.
5.1.1. Usage example
#include <boost/units/io.hpp>#include <boost/units/quantity.hpp>#include <boost/units/systems/si/length.hpp>#include <boost/units/systems/si/time.hpp>#include <boost/units/systems/si/velocity.hpp>#include <cassert>#include <iostream>namespace bu = boost :: units ; constexpr bu :: quantity < bu :: si :: velocity > avg_speed ( bu :: quantity < bu :: si :: length > d , bu :: quantity < bu :: si :: time > t ) { return d / t ; } void test () { const auto v = avg_speed ( 10 * bu :: si :: meters , 2 * bu :: si :: seconds ); assert ( v == 5 * bu :: si :: meters_per_second ); // passes assert ( v . value () == 5 ); // passes std :: cout << v << '\n' ; // prints "5 m s^-1" }
First thing to notice above is that a few headers have to be included just to make such a simple code to compile. Novices with Boost.Units library report this as an issue as sometimes it is not obvious why the code does not compile and which headers are missing.
Now, let us extend such a code sample for a real-life use case where we would like to pass a distance in kilometers or miles and duration in hours and get a velocity in those units.
#include <boost/units/base_units/metric/hour.hpp>#include <boost/units/base_units/us/mile.hpp>#include <boost/units/io.hpp>#include <boost/units/make_scaled_unit.hpp>#include <boost/units/quantity.hpp>#include <boost/units/systems/si/length.hpp>#include <boost/units/systems/si/time.hpp>#include <boost/units/systems/si/velocity.hpp>#include <boost/units/systems/si/prefixes.hpp>#include <cassert>#include <iostream>namespace bu = boost :: units ; using kilometer_base_unit = bu :: make_scaled_unit < bu :: si :: length , bu :: scale < 10 , bu :: static_rational < 3 >>>:: type ; using length_kilometer = kilometer_base_unit :: unit_type ; using length_mile = bu :: us :: mile_base_unit :: unit_type ; BOOST_UNITS_STATIC_CONSTANT ( miles , length_mile ); using time_hour = bu :: metric :: hour_base_unit :: unit_type ; BOOST_UNITS_STATIC_CONSTANT ( hours , time_hour ); using velocity_kilometers_per_hour = bu :: divide_typeof_helper < length_kilometer , time_hour >:: type ; BOOST_UNITS_STATIC_CONSTANT ( kilometers_per_hour , velocity_kilometers_per_hour ); using velocity_miles_per_hour = bu :: divide_typeof_helper < length_mile , time_hour >:: type ; BOOST_UNITS_STATIC_CONSTANT ( miles_per_hour , velocity_miles_per_hour ); constexpr bu :: quantity < bu :: si :: velocity > avg_speed ( bu :: quantity < bu :: si :: length > d , bu :: quantity < bu :: si :: time > t ) { return d / t ; } void test1 () { const auto v = avg_speed ( bu :: quantity < bu :: si :: length > ( 220 * bu :: si :: kilo * bu :: si :: meters ), bu :: quantity < bu :: si :: time > ( 2 * hours )); // assert(v.value() == 110); // fails bu :: quantity < velocity_kilometers_per_hour > kmph ( v ); // assert(kmph == 110 * kilometers_per_hour); // fails std :: cout << kmph << '\n' ; // prints "110 k(m h^-1)" } void test2 () { const auto v = avg_speed ( bu :: quantity < bu :: si :: length > ( 140 * miles ), bu :: quantity < bu :: si :: time > ( 2 * hours )); // assert(v.value() == 70); // fails bu :: quantity < velocity_miles_per_hour > mph ( v ); // assert(mph == 70 * miles_per_hour); // fails std :: cout << mph << '\n' ; // prints "70 mi h^-1" }
Even with such a simple example we immediately need to include even more headers and we have to define custom unit types and their constants for quantities that should be common and provided by the library for user’s convenience.
Also, please notice that both pairs of asserts fail. This is caused by the fact that this and many other units libraries implicitly convert all the units to the coherent derived units of their dimensions which impacts the runtime performance and precision. This is another common problem reported by users for Boost.Units. More information on this subject can be found at § 8 Limiting intermediate quantity value conversions).
To remove unnecessary conversions we will use a function template. The good part is it makes the assert to pass as there are no more intermediate conversions being done in both cases. However, the side effect of this change is an increased complexity of code which now is probably too hard to be implemented by a common C++ developer:
template < typename LengthSystem , typename Rep1 , typename TimeSystem , typename Rep2 > constexpr bu :: quantity < typename bu :: divide_typeof_helper < bu :: unit < bu :: length_dimension , LengthSystem > , bu :: unit < bu :: time_dimension , TimeSystem >>:: type > avg_speed ( bu :: quantity < bu :: unit < bu :: length_dimension , LengthSystem > , Rep1 > d , bu :: quantity < bu :: unit < bu :: time_dimension , TimeSystem > , Rep2 > t ) { return d / t ; } void test1 () { const auto v = avg_speed ( 220 * bu :: si :: kilo * bu :: si :: meters , 2 * hours ); assert ( v . value () == 110 ); // passes assert ( v == 110 * kilometers_per_hour ); // passes std :: cout << v << '\n' ; // prints "110 k(m h^-1)" } void test2 () { const auto v = avg_speed ( 140 * miles , 2 * hours ); assert ( v . value () == 70 ); // passes assert ( v == 70 * miles_per_hour ); // passes std :: cout << v << '\n' ; // prints "70 mi h^-1" }
The above example will be used as base for comparison to other units libraries described in the next chapters.
5.1.2. Design
Base dimensions are associated with tag types that have assigned a unique integer in order to be able to sort them on a list of a derived dimension. Negative ordinals are reserved for use by the library.
template < typename Derived , long N > class base_dimension : public ordinal < N > { public : typedef unspecified dimension_type ; typedef Derived type ; };
To define custom base dimension the user has to:
struct my_dimension : boost :: units :: base_dimension < my_dimension , 1 > {};
To define derived dimensions corresponding to the base dimensions, MPL-conformant
type lists of base dimensions must be created by using the
class to encapsulate
pairs of base dimensions and
exponents. The
class acts as a wrapper to ensure that the resulting type is in the form of a normalized dimension:
typedef make_dimension_list < boost :: mpl :: list < dim < length_base_dimension , static_rational < 1 >>> >:: type length_dimension ;
This can also be accomplished using a convenience typedef provided by
:
typedef length_base_dimension :: dimension_type length_dimension ;
To define the derived dimension similar steps have to be done:
typedef make_dimension_list < boost :: mpl :: list < dim < mass_base_dimension , static_rational < 1 >> , dim < length_base_dimension , static_rational < 2 >> , dim < time_base_dimension , static_rational <- 2 >>> >:: type energy_dimension ;
or
typedef derived_dimension < mass_base_dimension , 1 , length_base_dimension , 2 , time_base_dimension , - 2 >:: type energy_dimension ;
A unit is defined as a set of base units each of which can be raised to an arbitrary rational exponent. Units are, like dimensions, purely compile-time variables with no associated value.
template < typename Dim , typename System , typename Enable > class unit { public : typedef unit < Dim , System > unit_type ; typedef unit < Dim , System > this_type ; typedef Dim dimension_type ; typedef System system_type ; unit (); unit ( const this_type & ); BOOST_CXX14_CONSTEXPR this_type & operator = ( const this_type & ); };
In addition to supporting the compile-time dimensional analysis operations, the
,
,
, and
runtime operators are provided for unit variables.
Base units are defined much like base dimensions and again negative ordinals are reserved:
template < typename Derived , typename Dim , long N > class base_unit ;
To define a simple system of units:
struct meter_base_unit : base_unit < meter_base_unit , length_dimension , 1 > { }; struct kilogram_base_unit : base_unit < kilogram_base_unit , mass_dimension , 2 > { }; struct second_base_unit : base_unit < second_base_unit , time_dimension , 3 > { }; typedef make_system < meter_base_unit , kilogram_base_unit , second_base_unit >:: type mks_system ; typedef unit < dimensionless_type , mks_system > dimensionless ; typedef unit < length_dimension , mks_system > length ; typedef unit < mass_dimension , mks_system > mass ; typedef unit < time_dimension , mks_system > time ; typedef unit < area_dimension , mks_system > area ; typedef unit < energy_dimension , mks_system > energy ;
The macro
is provided to facilitate ODR- and thread-safe
constant definition in header files. With this some constants are defined for the supported
units to simplify variable definitions:
BOOST_UNITS_STATIC_CONSTANT ( meter , length ); BOOST_UNITS_STATIC_CONSTANT ( meters , length ); BOOST_UNITS_STATIC_CONSTANT ( kilogram , mass ); BOOST_UNITS_STATIC_CONSTANT ( kilograms , mass ); BOOST_UNITS_STATIC_CONSTANT ( second , time ); BOOST_UNITS_STATIC_CONSTANT ( seconds , time ); BOOST_UNITS_STATIC_CONSTANT ( square_meter , area ); BOOST_UNITS_STATIC_CONSTANT ( square_meters , area ); BOOST_UNITS_STATIC_CONSTANT ( joule , energy ); BOOST_UNITS_STATIC_CONSTANT ( joules , energy );
To provide a textual output of units specialize the
class for each
fundamental dimension tag:
template <> struct base_unit_info < meter_base_unit > { static std :: string name () { return "meter" ; } static std :: string symbol () { return "m" ; } };
and similarly for
and
.
It is possible to define a base unit as being a multiple of another base unit.
For example, the way that
is actually defined by the library is
along the following lines:
struct gram_base_unit : boost :: units :: base_unit < gram_base_unit , mass_dimension , 1 > {}; typedef scaled_base_unit < gram_base_unit , scale < 10 , static_rational < 3 >>> kilogram_base_unit ;
It is also possible to scale a unit as a whole, rather than scaling the individual base units which comprise it. For this purpose, the metafunction
is used:
typedef make_scaled_unit < si :: time , scale < 10 , static_rational <- 9 >>>:: type nanosecond ;
Interesting point to note here is that even though Boost.Units has a strong and deeply integrated support for systems of units it implements a US Customary Units in an SI system rather than as an independent system of units:
namespace us { struct yard_base_unit : public boost :: units :: base_unit < yard_base_unit , si :: meter_base_unit :: dimension_type , - 501 > { static const char * name (); static const char * symbol (); }; typedef scaled_base_unit < yard_base_unit , scale < 1760 , static_rational < 1 >>> mile_base_unit ; } template <> struct base_unit_info < us :: mile_base_unit > ;
Quantities are implemented by the
class template:
template < class Unit , class Y = double > class quantity ;
Operators
,
,
, and
are provided for algebraic operations between scalars and units, scalars and quantities, units and quantities, and between quantities.
Also, the standard set of boolean comparison operators (
,
,
,
,
, and
) are provided to allow comparison of quantities from the same system of units.
In addition, integral and rational powers and roots can be computed using the
and
non-member functions.
To provide conversions between different units the following macro has to be used:
BOOST_UNITS_DEFINE_CONVERSION_FACTOR ( foot_base_unit , meter_base_unit , double , 0.3048 );
The macro
specifies a conversion that will be applied
to a base unit when no direct conversion is possible. This can be used to make arbitrary
conversions work with a single specialization:
struct my_unit_tag : boost :: units :: base_unit < my_unit_tag , boost :: units :: force_type , 1 > {}; // define the conversion factor BOOST_UNITS_DEFINE_CONVERSION_FACTOR ( my_unit_tag , SI :: force , double , 3.14159265358979323846 ); // make conversion to SI the default. BOOST_UNITS_DEFAULT_CONVERSION ( my_unit_tag , SI :: force );
Boost.Units also allows to provide runtime-defined conversion factors with:
using boost :: units :: base_dimension ; using boost :: units :: base_unit ; static const long currency_base = 1 ; struct currency_base_dimension : base_dimension < currency_base_dimension , 1 > {}; typedef currency_base_dimension :: dimension_type currency_type ; template < long N > struct currency_base_unit : base_unit < currency_base_unit < N > , currency_type , currency_base + N > {}; typedef currency_base_unit < 0 > us_dollar_base_unit ; typedef currency_base_unit < 1 > euro_base_unit ; typedef us_dollar_base_unit :: unit_type us_dollar ; typedef euro_base_unit :: unit_type euro ; // an array of all possible conversions double conversion_factors [ 2 ][ 2 ] = { { 1.0 , 1.0 }, { 1.0 , 1.0 } }; double get_conversion_factor ( long from , long to ) { return ( conversion_factors [ from ][ to ]); } void set_conversion_factor ( long from , long to , double value ) { conversion_factors [ from ][ to ] = value ; conversion_factors [ to ][ from ] = 1.0 / value ; } BOOST_UNITS_DEFINE_CONVERSION_FACTOR_TEMPLATE (( long N1 )( long N2 ), currency_base_unit < N1 > , currency_base_unit < N2 > , double , get_conversion_factor ( N1 , N2 ));
This library is designed to emphasize safety above convenience when performing operations with dimensioned quantities. Specifically:
-
construction of quantities is required to fully specify both value and unit
-
direct construction from a scalar value is prohibited (though the static member function
is provided to enable this functionality where it is necessary)from_value -
to a reference allows direct access to the underlying value of a quantity variablequantity_cast -
an explicit constructor is provided to enable conversion between dimensionally compatible quantities in different unit systems
-
implicit conversions between systems of units are allowed only when the normalized dimensions are identical, allowing, for example, trivial conversions between equivalent units in different systems (such as SI seconds and CGS seconds) while simultaneously enabling unintentional unit system mismatches to be caught at compile time and preventing potential loss of precision and performance overhead from unintended conversions
-
assignment follows the same rules
-
an exception is made for quantities for which the unit reduces to a dimensionless quantity; in this case, implicit conversion to the underlying value type is allowed via class template specialization
-
quantities of different value types are implicitly convertible only if the value types are themselves implicitly convertible
-
the quantity class also defines a
member for directly accessing the underlying valuevalue ()
There are two distinct types of systems that can be envisioned:
-
Homogeneous systems
Systems which hold a linearly independent set of base units which can be used to represent many different dimensions. For example, the SI system has seven base dimensions and seven base units corresponding to them. It can represent any unit which uses only those seven base dimensions. Thus it is a homogeneous_system.
-
Heterogeneous systems
Systems which store the exponents of every base unit involved are termed heterogeneous. Some units can only be represented in this way. For example, area in
is intrinsically heterogeneous, because the base units of meters and feet have identical dimensions. As a result, simply storing a dimension and a set of base units does not yield a unique solution. A practical example of the need for heterogeneous units, is an empirical equation used in aviation:m ft
whereH = ( r / C ) ^ 2
is the radar beam height in feet andH
is the radar range in nautical miles. In order to enforce dimensional correctness of this equation, the constant,r
, must be expressed in nauticalC
, mixing two distinct base units of length.miles per foot ^ ( 1 / 2 )
namespace cgs { typedef scaled_base_unit < boost :: units :: si :: meter_base_unit , scale < 10 , static_rational <- 2 >>> centimeter_base_unit ; typedef make_system < centimeter_base_unit , gram_base_unit , boost :: units :: si :: second_base_unit , biot_base_unit >:: type system ; }
quantity < si :: area > A ( 1.5 * si :: meter * cgs :: centimeter ); std :: cout << 1.5 * si :: meter * cgs :: centimeter << std :: endl // prints 1.5 cm m << A << std :: endl // prints 0.015 m^2 << std :: endl ;
To provide temperature support Boost.Units define 2 new systems:
namespace celsius { typedef make_system < boost :: units :: temperature :: celsius_base_unit >:: type system ; typedef unit < temperature_dimension , system > temperature ; static const temperature degree ; static const temperature degrees ; } namespace fahrenheit { typedef make_system < boost :: units :: temperature :: fahrenheit_base_unit >:: type system ; typedef unit < temperature_dimension , system > temperature ; static const temperature degree ; static const temperature degrees ; }
and a wrapper for handling absolute units (points rather than vectors) to provide affine space support:
template < typename Y > class absolute { public : // types typedef absolute < Y > this_type ; typedef Y value_type ; // construct/copy/destruct absolute (); absolute ( const value_type & ); absolute ( const this_type & ); BOOST_CXX14_CONSTEXPR this_type & operator = ( const this_type & ); // public member functions BOOST_CONSTEXPR const value_type & value () const ; BOOST_CXX14_CONSTEXPR const this_type & operator += ( const value_type & ); BOOST_CXX14_CONSTEXPR const this_type & operator -= ( const value_type & ); };
With above we can:
template < class From , class To > struct conversion_helper { static BOOST_CONSTEXPR To convert ( const From & ); }; typedef conversion_helper < quantity < absolute < fahrenheit :: temperature >> , quantity < absolute < si :: temperature >>> absolute_conv_type ; typedef conversion_helper < quantity < fahrenheit :: temperature > , quantity < si :: temperature >> relative_conv_type ; quantity < absolute < fahrenheit :: temperature >> T1p ( 32.0 * absolute < fahrenheit :: temperature > ()); quantity < fahrenheit :: temperature > T1v ( 32.0 * fahrenheit :: degrees ); quantity < absolute < si :: temperature >> T2p ( T1p ); quantity < si :: temperature > T2v ( T1v ); std :: cout << T1p << std :: endl // prints 32 absolute F << absolute_conv_type :: convert ( T1p ) << std :: endl // prints 273.15 absolute K << T2p << std :: endl // prints 273.15 absolute K << T1v << std :: endl // prints 32 F << relative_conv_type :: convert ( T1v ) << std :: endl // prints 17.7778 K << T2v << std :: endl // prints 17.7778 K << std :: endl ;
5.2. cppnow17-units
Steven Watanabe, the coauthor of the previous library, started the work on the modernized version of the library based on the results of LiaW on C++Now 2017 [CPPNOW17-UNITS]. As the library was never finished we will not discuss it in details.
5.2.1. Design
The main design is similar to [BOOST.UNITS] with one important difference - no systems. Steven Watanabe provided the following rationale for this design change:
"My take is that a system is essentially a set of units with linearly independent dimensions and this can be implemented as a convenience on top of the core functionality. Boost.Units started out with a design based solely on systems, but that proved to be too inflexible. We added support for combining individual units, similar to current libraries. However, having both systems and base units supported directly in the core library results in a very convoluted design and is one of the main issues that I wanted to fix in a new library."
Another interesting design change is the approach for temperatures. With the new design Celsius and Fahrenheit are always treated as absolute temperatures and only Kelvins can act as an absolute or relative value.
kelvin + kelvin = kelvin celsius - celsius = kelvin celsius + kelvin = celsius
5.3. PhysUnits-CT-Cpp11
[PHYSUNITS-CT-CPP11] is the library based on the work of Michael Kenniston from 2001 and expanded and adapted for C++11 by Martin Moene.
5.3.1. Usage example
#include <phys/units/io.hpp>#include <phys/units/quantity.hpp>#include <phys/units/other_units.hpp>#include <iostream>#include <cassert>namespace pu = phys :: units ; using namespace pu :: literals ; using namespace phys :: units :: io ; constexpr pu :: quantity < pu :: speed_d > avg_speed ( pu :: quantity < pu :: length_d > d , pu :: quantity < pu :: time_interval_d > t ) { return d / t ; } void test1 () { constexpr auto v = avg_speed ( 220 _km , 2 * pu :: hour ); // assert(v.magnitude() == 110); // fails assert ( v == 110 _km / pu :: hour ); // passes std :: cout << v << '\n' ; // prints "30.5556 m/s" } void test2 () { constexpr auto v = avg_speed ( 140 * pu :: mile , 2 * pu :: hour ); // assert(v.magnitude() == 70); // fails assert ( v == 70 * pu :: mile / pu :: hour ); // passes std :: cout << v << '\n' ; // prints "31.2928 m/s" }
Please note that this library is a pretty simple library and thus has a lot limitations:
-
We are unable to pass arguments to
in units provided by the user because quantities are always converted to base units (so there is no need to try to make it a function template).avg_speed -
Because of above we also do not get the result in the unit we would like. This it why the first assert fails.
-
There is no possibility to cast returned quantity to the unit that we would like to use for printing.
-
Because of always forced intermediate conversions to base units the second assert passes even though the result’s precision is degraded (both sides of equality are the same broken).
5.3.2. Design
The library defines dimensions such as
and
as a list of 7 template
parameters representing exponents of each SI dimension:
#ifdef PHYS_UNITS_REP_TYPE using Rep = PHYS_UNITS_REP_TYPE ; #else using Rep = double ; #endif template < int D1 , int D2 , int D3 , int D4 = 0 , int D5 = 0 , int D6 = 0 , int D7 = 0 > struct dimensions { template < int R1 , int R2 , int R3 , int R4 , int R5 , int R6 , int R7 > constexpr bool operator == ( dimensions < R1 , R2 , R3 , R4 , R5 , R6 , R7 > const & ) const ; template < int R1 , int R2 , int R3 , int R4 , int R5 , int R6 , int R7 > constexpr bool operator != ( dimensions < R1 , R2 , R3 , R4 , R5 , R6 , R7 > const & rhs ) const ; }; typedef dimensions < 0 , 0 , 0 > dimensionless_d ; typedef dimensions < 1 , 0 , 0 , 0 , 0 , 0 , 0 > length_d ; typedef dimensions < 0 , 1 , 0 , 0 , 0 , 0 , 0 > mass_d ; typedef dimensions < 0 , 0 , 1 , 0 , 0 , 0 , 0 > time_interval_d ; typedef dimensions < 0 , 0 , 0 , 1 , 0 , 0 , 0 > electric_current_d ; typedef dimensions < 0 , 0 , 0 , 0 , 1 , 0 , 0 > thermodynamic_temperature_d ; typedef dimensions < 0 , 0 , 0 , 0 , 0 , 1 , 0 > amount_of_substance_d ; typedef dimensions < 0 , 0 , 0 , 0 , 0 , 0 , 1 > luminous_intensity_d ;
Quantities represent their units (
,
, ...):
template < typename Dims , typename T = Rep > class quantity { /* ... */ }; // The seven SI base units. These tie our numbers to the real world. constexpr quantity < length_d > meter { detail :: magnitude_tag , 1.0 }; constexpr quantity < mass_d > kilogram { detail :: magnitude_tag , 1.0 }; constexpr quantity < time_interval_d > second { detail :: magnitude_tag , 1.0 }; constexpr quantity < electric_current_d > ampere { detail :: magnitude_tag , 1.0 }; constexpr quantity < thermodynamic_temperature_d > kelvin { detail :: magnitude_tag , 1.0 }; constexpr quantity < amount_of_substance_d > mole { detail :: magnitude_tag , 1.0 }; constexpr quantity < luminous_intensity_d > candela { detail :: magnitude_tag , 1.0 };
Derived dimensions and units are defined in the same way:
// The rest of the standard dimensional types, as specified in SP811. using absorbed_dose_d = dimensions < 2 , 0 , - 2 > ; using absorbed_dose_rate_d = dimensions < 2 , 0 , - 3 > ; using acceleration_d = dimensions < 1 , 0 , - 2 > ; using activity_of_a_nuclide_d = dimensions < 0 , 0 , - 1 > ; using angular_velocity_d = dimensions < 0 , 0 , - 1 > ; using angular_acceleration_d = dimensions < 0 , 0 , - 2 > ; using area_d = dimensions < 2 , 0 , 0 > ; using capacitance_d = dimensions <- 2 , - 1 , 4 , 2 > ; using concentration_d = dimensions <- 3 , 0 , 0 , 0 , 0 , 1 > ; // ... // The derived SI units, as specified in SP811. constexpr Rep radian { Rep ( 1 )}; constexpr Rep steradian { Rep ( 1 )}; constexpr quantity < force_d > newton { meter * kilogram / square ( second )}; constexpr quantity < pressure_d > pascal { newton / square ( meter )}; constexpr quantity < energy_d > joule { newton * meter }; constexpr quantity < power_d > watt { joule / second }; // ...
The library also provides UDLs for SI units and their prefixes ranging from
to
. Thus it is possible to write quantity literals such as
and
.
5.4. Nic Holthaus units
The next is C++14 library created by Nic Holthaus [NIC_UNITS].
5.4.1. Usage example
#include <include/units.h>#include <type_traits>#include <iostream>#include <cassert>template < typename Length , typename Time , typename = std :: enable_if_t < units :: traits :: is_length_unit < Length >:: value && units :: traits :: is_time_unit < Time >:: value >> constexpr auto avg_speed ( Length d , Time t ) { static_assert ( units :: traits :: is_velocity_unit < decltype ( d / t ) >:: value ); return d / t ; } using namespace units :: literals ; void test1 () { const auto v = avg_speed ( 220 _km , 2 _hr ); assert ( v . value () == 110 ); // passes assert ( v == 110 _kph ); // passes std :: cout << v << '\n' ; // prints "30.5556 m s^-1" } void test2 () { const auto v = avg_speed ( units :: length :: mile_t ( 140 ), units :: time :: hour_t ( 2 )); assert ( v . value () == 70 ); // passes assert ( v == units :: velocity :: miles_per_hour_t ( 70 )); // passes std :: cout << v << '\n' ; // prints "31.2928 m s^-1" }
An interesting usability point to note here is the fact that we cannot provide partial
definition of quantity types in a function template. It is caused by the usage of unit
nesting which makes it impossible to determine on which level of nesting we will find
a dimension tag (i.e.
):
namespace length { using meters = units :: unit < std :: ratio < 1 > , units :: category :: length_unit > ; using feet = units :: unit < std :: ratio < 381 , 1250 > , meters > ; }
This is why we can either provide a specific type that will force intermediate
conversions or just use
template parameter for function arguments.
template < typename Length , typename Time > constexpr auto avg_speed ( Length d , Time t )
We can try to SFINAE other types using provided type traits (see the usage example above) but it is not the most user-friendly solution and most of them will probably not use it for their daily code.
Also please note that even though the returned type is what we would expect (a velocity in a correct unit) and there are no intermediate conversions, it is being printed in terms of base units which is not what is expected by the user.
5.4.2. Design
The library consists of a single file (
) with the ability to remove some parts
of unneeded functionality with preprocessor macros (i.e. to speed up compilation time).
It provides a set of types, containers, and traits to solve dimensional analysis problems.
Each dimension is defined in its own namespace.
Unit tags are the foundation of the unit library. Unit tags are types which are never instantiated in user code, but which provide the meta-information about different units, including how to convert between them, and how to determine their compatibility for conversion.
namespace units { template < class Meter = detail :: meter_ratio < 0 > , class Kilogram = std :: ratio < 0 > , class Second = std :: ratio < 0 > , class Radian = std :: ratio < 0 > , class Ampere = std :: ratio < 0 > , class Kelvin = std :: ratio < 0 > , class Mole = std :: ratio < 0 > , class Candela = std :: ratio < 0 > , class Byte = std :: ratio < 0 >> struct base_unit ; }
Interesting to notice here is that beside typical SI dimensions, there are also
and
.
Units in the library are defined in terms of:
-
a scale factor relative to a base unit type
-
[optionally] a scale factor of
required by the conversion (i.e.pi
for a radians to degrees conversion)std :: ratio <- 1 > -
[optionally] a datum translation (i.e.
for a Fahrenheit to Celsius conversion)std :: ratio < 32 >
namespace units { template < class Conversion , class BaseUnit , class PiExponent = std :: ratio < 0 > , class Translation = std :: ratio < 0 >> struct unit ; }
All units have their origin in the SI. A special exception is made for angle units,
which are defined in SI as (
), and in this library they are treated as a
base unit type because of their important engineering applications.
Quantities are represented in this library as unit containers that are the primary
classes which will be instantiated in user code. Containers are derived from the
class, and have the form [unitname]_t, e.g.
or
.
namespace units { template < class Units , typename T = UNIT_LIB_DEFAULT_TYPE , template < typename > class NonLinearScale = linear_scale > class unit_t : public NonLinearScale < T > { ... }; }
One more interesting point to notice here is that this library is using
to
report conversion errors rather than to relay on an overload resolution process (and SFINAE).
The side effects of this are:
-
being a QoI tool is not a part of function signature thus does not influence overload resolutionstatic_assert -
provides a short error message which might be a good thing but it also often misses information about the source of the problem. For example below error does not provide any information about the source and destination errors:static_assert error: static assertion failed: Units are not compatible. static_assert(traits::is_convertible_unit<UnitFrom, UnitTo>::value, "Units are not compatible."); ^~~~~~
5.5. benri
[BENRI] is a library written by Jan A. Sende and provides wide support for many systems of units, physical constants, mathematic operations, and affine spaces.
5.5.1. Usage example
#include <benri/si/imperial.h>#include <benri/si/si.h>#include <iostream>#include <cassert>template < class Length , class Time , typename = std :: enable_if_t < benri :: type :: detect_if < Length , benri :: type :: has_dimension , benri :: dimension :: length_t > && benri :: type :: detect_if < Time , benri :: type :: has_dimension , benri :: dimension :: time_t >>> constexpr auto avg_speed ( const Length & length , const Time & time ) { const auto ret = length / time ; static_assert ( benri :: type :: detect_if < decltype ( ret ), benri :: type :: has_dimension , benri :: dimension :: velocity_t > ); return ret ; } void test1 () { using namespace benri :: si ; const auto v = avg_speed ( 220 _kilo * metre , 2 _hour ); assert ( v . value () == 110 ); // passes assert ( v == 110 * kilo * metre / hour ); // passes // std::cout << v << '\n'; // no support } void test2 () { using namespace benri :: si ; const auto v = avg_speed ( 140 * imperial :: mile , 2 * hour ); assert ( v . value () == 70 ); // passes assert ( v == 70 * imperial :: mile / hour ); // passes // std::cout << v << '\n'; // no support }
Above usage example is quite similar to the one in § 5.4.1 Usage example as both
libraries do not support function template arguments deduction for
class template
function arguments to improve overload resolution process. The interface architect has to use
SFINAE to achieve that.
On contrary to [NIC_UNITS] this library does not provide short predefined UDLs. UDLs are
defined only for prefixes and long names of named units while the user needs to compose
all other derived units by him/herself (i.e.
). This makes this
library to be similar to [BOOST.UNITS] in that aspect.
Interesting point to also notice here is that the library intentionally does not provide text output support and leaves that work to the user. Beside forcing every user to reinvent the wheel this approach might also result in some issues:
-
ODR violations (several dependency libraries might reimplement the same operator)
-
lookup problems (if user will not define the operators in
orstd
namespace those will not be found via ADL from a generic code)benri
5.5.2. Design
The
type implements the physics concept of a unit which is the product of a
prefix and a number of base dimensions with an associated power:
template < class Dimension , class Prefix > struct unit ;
where both
and
are sorted type lists. Representing a prefix as a
type list is unique to this library and is meant to address limited range of
.
Quantities are addressed with two distinct types that are used to provide affine space support:
template < class Unit , class ValueType = Precision > class quantity ; template < class Unit , class ValueType = Precision > class quantity_point ;
This library puts usage safety over user’s convenience. The effect of this is that even obvious conversions require explicit casts on assignment, arithmetic operations, and comparisons:
// auto a = 1_metre + 10_centi * metre; // does not compile // assert(a < 10_metre); // does not compile auto a = benri :: simple_cast < decltype ( centi * metre ) > ( 1 _metre ) + 10 _centi * metre ; assert ( a < benri :: simple_cast < decltype ( centi * metre ) > ( 10 _metre ));
It can also be noted here that this library enforces AAA (Almost Always Auto) programming
style as due to limited number of predefined derived units it is often impossible to clearly
provide exact unit in a
type:
const auto speed = a * metre / b * second ;
5.6. Other
There are more smaller units solutions out there. The author reviewed also the following libraries:
-
Michael Ford units ([MIKEFORD3_UNITS])
-
Bryan St. Amour units ([BRYAN_UNITS])
-
Vincent Ducharme units ([DUCHARME_UNITS])
5.7. Comparison
Feature | mp-units | Boost.Units | PhysUnits-CT-Cpp11 | nholthaus | benri |
---|---|---|---|---|---|
SI | yes | yes | yes | yes | yes |
Customary system | yes | yes | some | yes | yes |
Other systems | yes | yes | no | yes ( , )
| yes |
C++ version | C++20 | C++98 +
| C++11 | C++14 | C++14 |
Base dimension id |
| integer | index on template parameter list | index on template parameter list | string |
Dimension | type ( )
| alias to type list ( )
| alias to type list ( )
| namespace ( )
| alias to type list ( )
|
Dimension representation | type list | type list | class template arguments | class template arguments | type list |
Fractional exponents | yes | yes | no | yes | yes |
Type traits for dimensions | no | yes | no | yes | some |
Unit | type ( )
| alias + constant ( + )
| value ( )
| alias ( )
| alias ( )
|
UDLs | yes | no | some | yes | yes (long form only i.e. )
|
Composable Units | no | prefix only ( )
| prefix only ( )
| no | yes ( )
|
Predefined scaled unit types | some | no | some | all | no |
Scaled units | type + UDL ( + )
| predefined values or multiplied with a prefix ( )
| value + UDL ( + )
| type + UDL ( + )
| no |
Meter vs metre | metre | both | both | meter | metre |
Singular vs plural | singular ( )
| both ( + )
| singular | both ( + )
| singular ( )
|
Quantity | type ( )
| type ( )
| type ( )
| type ( )
| type ( )
|
Literal instance | UDL ( )
| Number * static constant ( )
| UDL ( )
| UDL ( )
| UDL ( )
|
Variable instance | constructor ( )
| Variable * static constant ( )
| Variable * static constant ( )
| constructor ( )
| constructor ( )
|
Any representation | yes | yes | yes | no (macro to set the default type) | yes (macro default of )
|
Quantity template arguments type deduction | yes | yes | no | no | no |
Dedicated system abstraction | no | yes | no | no | no |
C++ Concepts | yes | no | no | no | no |
Types downcasting | yes | no | no | no | no |
Implicit unit conversions | same dimension non-truncating only | no | same dimension | same dimension | no |
Explicit unit conversions |
|
| yes | no | ( )/
|
Runtime conversion factors | no | yes | no | no | no |
Temperature support | yes | Kelvins in SI + other in dedicated systems | relative values only | absolute values only | yes |
String output | yes | yes | yes | yes | no |
support
| yes | no | no | no | no |
String input | no | no | no | no | no |
Macros in the user interface | no | yes | no | yes | yes |
Non-linear scale support | no | no | no | yes | no |
support
| TBD | yes | no | yes | yes |
support
| ??? | no | no | yes | yes |
Affine types | yes ( , )
| yes ( )
| no | no | yes ( , )
|
Prefix representation |
|
| long double | ratio | type list |
Physical/Mathematical constants | TBD | yes | limited | limited | all |
Dimensionless quantity | yes ( )
| yes ( )
| yes ( )
| yes ( )
| yes ( )
|
Arbitrary conversions | yes | yes | yes | no | yes |
User defined base dimensions | yes | yes | no | no | yes |
User defined derived dimensions | yes | yes | yes | yes | yes |
User defined units | yes | yes | yes | yes | yes |
User defined prefixes | yes | yes | yes | yes | yes |
6. Fundamental concerns with current solutions
Feedback from the users gathered so far signals the following significant complaints regarding the libraries described in § 5 Prior Work:
-
Bad user experience caused by hard to understand and analyze compile-time errors and poor debugging experience (addressed by § 7 Improving user experience).
-
Unnecessary intermediate quantity value conversions to base units resulting in a runtime overhead and loss of precision (addressed by § 8 Limiting intermediate quantity value conversions).
-
Poor support for really large or small unit ratios (i.e.
) (addressed by § 9 std::ratio on steroids).eV -
Impossibility or hard extensibility of the library with new base quantities (addressed by § 11 Extensibility).
-
Too high entry bar (e.g. Boost.Units is claimed to require expertise in both C++ and dimensional analysis) (addressed by § 12 Easy to use and hard to abuse).
-
Safety and security connected problems with the usage of an external 3rd party library for production purposes (addressed by § 3.1 Motivation).
7. Improving user experience
7.1. Type aliasing issues
Type aliases benefit developers but not end-users. As a result users end up with colossal error messages.
Taking Boost.Units as an example, the code developer works with the following syntax:
namespace bu = boost :: units ; constexpr bu :: quantity < bu :: si :: velocity > avg_speed ( bu :: quantity < bu :: si :: length > d , bu :: quantity < bu :: si :: time > t ) { return d * t ; }
Above calculation contains a simple error as a velocity derived quantity cannot be created from multiplication of length and time base quantities. If such an error happens in the source code, user will need to analyze the following error for gcc-8:
error: could not convert ‘boost::units::operator*(const boost::units::quantity<Unit1, X>&, const boost::units::quantity<Unit2, Y>&) [with Unit1 = boost::units::unit<boost::units::list<boost::units::dim <boost::units::length_base_dimension, boost::units::static_rational<1> >, boost::units::dimensionless_type>, boost::units::homogeneous_system<boost::units::list<boost::units::si::meter_base_unit, boost::units::list<boost::units::scaled_base_unit<boost::units::cgs::gram_base_unit, boost::units::scale<10, boost::units::static_rational<3> > >, boost::units::list<boost::units::si::second_base_unit, boost::units::list<boost::units::si::ampere_base_unit, boost::units::list<boost::units::si::kelvin_base_unit, boost::units::list<boost::units::si::mole_base_unit, boost::units::list<boost::units::si::candela_base_unit, boost::units::list<boost::units::angle::radian_base_unit, boost::units::list<boost::units::angle::steradian_base_unit, boost::units::dimensionless_type> > > > > > > > > > >; Unit2 = boost::units::unit<boost::units::list<boost::units::dim <boost::units::time_base_dimension, boost::units::static_rational<1> >, boost::units::dimensionless_type>, boost::units::homogeneous_system<boost::units::list<boost::units::si::meter_base_unit, boost::units::list<boost::units::scaled_base_unit<boost::units::cgs::gram_base_unit, boost::units::scale<10, boost::units::static_rational<3> > >, boost::units::list<boost::units::si::second_base_unit, boost::units::list <boost::units::si::ampere_base_unit, boost::units::list<boost::units::si::kelvin_base_unit, boost::units::list <boost::units::si::mole_base_unit, boost::units::list<boost::units::si::candela_base_unit, boost::units::list <boost::units::angle::radian_base_unit, boost::units::list<boost::units::angle::steradian_base_unit, boost::units::dimensionless_type> > > > > > > > > > >; X = double; Y = double; typename boost::units::multiply_typeof_helper<boost::units::quantity<Unit1, X>, boost::units::quantity<Unit2, Y> >::type = boost::units::quantity<boost::units::unit<boost::units::list<boost::units::dim<boost::units::length_base_dimension, boost::units::static_rational<1> >, boost::units::list<boost::units::dim<boost::units::time_base_dimension, boost::units::static_rational<1> >, boost::units::dimensionless_type> >, boost::units::homogeneous_system <boost::units::list<boost::units::si::meter_base_unit, boost::units::list<boost::units::scaled_base_unit <boost::units::cgs::gram_base_unit, boost::units::scale<10, boost::units::static_rational<3> > >, boost::units::list<boost::units::si::second_base_unit, boost::units::list<boost::units::si::ampere_base_unit, boost::units::list<boost::units::si::kelvin_base_unit, boost::units::list<boost::units::si::mole_base_unit, boost::units::list<boost::units::si::candela_base_unit, boost::units::list<boost::units::angle::radian_base_unit, boost::units::list<boost::units::angle::steradian_base_unit, boost::units::dimensionless_type> > > > > > > > > >, void>, double>](t)’ from ‘quantity<unit<list<[...],list<dim<[...],static_rational<1>>,[...]>>,[...],[...]>,[...]>’ to ‘quantity<unit<list<[...],list<dim<[...],static_rational<-1>>,[...]>>,[...],[...]>,[...]>’ return d * t; ~~^~~
An important point to notice here is that above text is just the very first line of the compilation error log. Error log for the same problem generated by clang-7 looks as follows:
error: no viable conversion from returned value of type 'quantity<unit<list<[...], list<dim<[...], static_rational<1, [...]>>, [...]>>, [...]>, [...]>' to function return type 'quantity<unit<list<[...], list<dim<[...], static_rational<-1, [...]>>, [...]>>, [...]>, [...]>' return d * t; ^~~~~
Despite being shorter, this message does not really help much in finding the actual fault too.
Omnipresent type aliasing does not affect only compilation errors observed by the end-user but also debugging. Here is how a breakpoint for the above function looks like in the gdb debugger:
Breakpoint 1, avg_speed<boost::units::heterogeneous_system<boost::units::heterogeneous_system_impl <boost::units::list<boost::units::heterogeneous_system_dim<boost::units::si::meter_base_unit, boost::units::static_rational<1> >, boost::units::dimensionless_type>, boost::units::list<boost::units::dim<boost::units::length_base_dimension, boost::units::static_rational<1> >, boost::units::dimensionless_type>, boost::units::list<boost::units::scale_list_dim <boost::units::scale<10, boost::units::static_rational<3> > >, boost::units::dimensionless_type> > >, boost::units::heterogeneous_system<boost::units::heterogeneous_system_impl<boost::units::list <boost::units::heterogeneous_system_dim<boost::units::scaled_base_unit<boost::units::si::second_base_unit, boost::units::scale<60, boost::units::static_rational<2> > >, boost::units::static_rational<1> >, boost::units::dimensionless_type>, boost::units::list<boost::units::dim<boost::units::time_base_dimension, boost::units::static_rational<1> >, boost::units::dimensionless_type>, boost::units::dimensionless_type> > > (d=..., t=...) at velocity_2.cpp:39 39 return d / t;
7.2. Downcasting facility
To provide much shorter error messages the author of the paper with the help of Richard Smith, implemented a downcast facility in [MP-UNITS]. It allowed converting the following error log from:
[with T = units::quantity<units::detail::derived_dimension_base<units::exp<units::si::dim_electric_current, 2, 1>, units::exp<units::si::dim_length, -2, 1>, units::exp<units::si::dim_mass, -1, 1>, units::exp<units::si::dim_time, 4, 1> >, units::scaled_unit<std::ratio<1>, units::si::farad>, double>]
into:
[with T = units::quantity<units::si::dim_capacitance, units::si::farad, double>]
As a result the type dumped in the error log is exactly the same entity that the developer used to implement the erroneous source code.
The above is possible thanks to the fact that the downcasting facility provides a type substitution mechanism. It connects a specific primary class template instantiation with a strong type assigned by the user. A simplified mental model of the facility may be represented as:
struct velocity : derived_dimension < exp < base_dim_length , 1 > , exp < base_dim_time , - 1 >> ; struct metre_per_second : unit <> ;
In the above example,
and
are the downcasting targets
(child classes), and specific
and
class template instantiations
are downcasting sources (base classes). The downcasting facility provides one to one
type substitution mechanism for those types. This means that only one child class can
be created for a specific base class template instantiation.
The downcasting facility is provided through two dedicated types, a concept, and a few helper template aliases.
template < typename BaseType > struct downcast_base { using downcast_base_type = BaseType ; friend auto downcast_guide ( downcast_base ); };
is a class that implements the CRTP idiom, marks the base of the
downcasting facility with a
member type, and provides a declaration
of the downcasting ADL friendly (Hidden Friend) entry point member function
.
An important design point is that this function does not return any specific type in its
declaration. This non-member function is going to be defined in a child class template
and will return a target type of the downcasting operation there.
template < typename T > concept Downcastable = requires { typename T :: downcast_base_type ; } && std :: derived_from < T , downcast_base < typename T :: downcast_base_type >> ;
is a concept that verifies if a type implements and can be used in
a downcasting facility.
template < typename Target , Downcastable T > struct downcast_child : T { friend auto downcast_guide ( typename downcast_child :: downcast_base ) { return Target (); } };
is another CRTP class template that provides the implementation
of a non-member friend function of the
class template, which defines the target
type of a downcasting operation. It is used in the following way to define
and
types in the library:
template < typename Child , Unit U , Exponent E , Exponent ... ERest > struct derived_dimension : downcast_child < Child , typename detail :: make_dimension < E , ERest ... >> { using recipe = exp_list < E , ERest ... > ; using coherent_unit = U ; using base_units_ratio = detail :: base_units_ratio < derived_dimension > ; };
template < typename Child , basic_fixed_string Symbol , PrefixType PT > struct named_unit : downcast_child < Child , scaled_unit < ratio < 1 > , Child >> { static constexpr bool is_named = true; static constexpr auto symbol = Symbol ; using prefix_type = PT ; };
With such helper types, the only thing the user has to do is to register a new type for the downcasting facility by publicly deriving from one of those CRTP types and provide its new child type as the first template parameter of the CRTP type:
struct metre_per_second : unit < metre_per_second > {}; struct dim_velocity : derived_dimension < dim_velocity , metre_per_second , exp < dim_length , 1 > , exp < dim_time , - 1 >> {};
The above types are used to define the base and target of a downcasting operation. To perform the actual downcasting operation, a dedicated template alias is provided and used by the library’s framework:
template < Downcastable T > using downcast = decltype ( detail :: downcast_target_impl < T > ());
is used to obtain the target type of the downcasting operation registered
for a given instantiation in a base type.
checks if a downcasting target is registered for the specific
base class. If registered,
returns the registered type,
otherwise it returns the provided base class.
namespace detail { template < typename T > concept has_downcast = requires { downcast_guide ( std :: declval < downcast_base < T >> ()); }; template < typename T > constexpr auto downcast_target_impl () { if constexpr ( has_downcast < T > ) return decltype ( downcast_guide ( std :: declval < downcast_base < T >> ()))(); else return T (); } }
7.3. Template instantiation issues
C++ is known for massive error logs caused by compilation errors deep down in the stack of
function template instantiations of an implementation. In the vast majority of cases, this
is caused by function templates just taking a
as their parameter, not placing
any constraints on the actual type. In C++17 placing such constraints is possible thanks
to SFINAE and helpers like
or
. However, these are known to be
not really user-friendly.
Consider the following example:
template < typename Length , typename Time , typename = std :: enable_if_t < units :: traits :: is_length_unit < Length >:: value && units :: traits :: is_time_unit < Time >:: value >> constexpr auto avg_speed ( Length d , Time t ) -> std :: enable_if_t < units :: traits :: is_velocity_unit < decltype ( d / t ) >:: value > , decltype ( d / t ) > { const auto v = d / t ; static_assert ( units :: traits :: is_velocity_unit < decltype ( v ) >:: value ); return v ; }
Clearly this is not the most user-friendly way to write code every day. Imagine the effort involved for C++ experts and non-experts alike to write longer and more complex functions, multiline calculations, or even whole programs in this style. Obviously C++20 concepts radically simplify the boiler plate involved and are thus the way to go.
7.4. Better errors with C++20 concepts
With C++20 concepts above example is simplified to:
template < units :: Length L , units :: Time T > constexpr units :: Velocity auto avg_speed ( L d , T t ) { return d / t ; }
Using generic functions, it can even be implemented, without the template syntax, as:
constexpr units :: Velocity auto avg_speed ( units :: Length auto d , units :: Time auto t ) { return d / t ; }
Thanks to C++20 concepts we not only get much stronger interfaces with their compile-time contracts clearly expressed by concepts in the function template signature, but also much better error logs. Concept constraint validation being done early in the function instantiation process catches errors early and not deep in the instantiation stack, significantly improving the readability of the actual errors.
For example, the latest svn version of gcc-10 generates the following error message:
hello_units.cpp: In instantiation of ‘constexpr auto [requires units::Velocity<<placeholder>, >] avg_speed(L, T) [with L = units::quantity<units::si::dim_length, units::si::kilometre, long int>; T = units::quantity<units::si::dim_time, units::si::hour, long int>]’: hello_units.cpp:39:32: required from here hello_units.cpp:33:14: error: deduced return type does not satisfy placeholder constraints 33 | return d * t; | ^ hello_units.cpp:33:14: note: constraints not satisfied In file included from units/physical/si/velocity.h:25, from hello_units.cpp:23: units/physical/dimensions.h:38:9: required for the satisfaction of ‘QuantityOf<units::quantity<units::unknown_dimension<units::exp<units::si::dim_length, 1, 1>, units::exp<units::si::dim_time, 1, 1> >, units::scaled_unit<units::ratio<36, 1, 5>, units::unknown_unit>, long int>, units::physical::dim_velocity>’ units/physical/dimensions.h:137:9: required for the satisfaction of ‘Velocity<units::quantity<units::unknown_dimension<units::exp<units::si::dim_length, 1, 1>, units::exp<units::si::dim_time, 1, 1> >, units::scaled_unit<units::ratio<36, 1, 5>, units::unknown_unit>, long int> >’ units/physical/dimensions.h:38:37: note: the expression ‘is_derived_from_instantiation<typename Q::dimension, DimTemplate>’ evaluated to ‘false’ 38 | concept QuantityOf = Quantity<Q> && is_derived_from_instantiation<typename Q::dimension, DimTemplate>; | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
While still being a little verbose, this is a big improvement to the page-long instantiation lists shown above. The user gets the exact information of what was wrong with the provided type, why it did not meet the required constraints, and where the error occurred. With concept support still being experimental, we expect error message to improve even more in the future.
8. Limiting intermediate quantity value conversions
Many of the physical units libraries on the market decide to quietly convert different units to the one fixed, coherent derived unit of the dimension. For example:
namespace bu = boost :: units ; constexpr bu :: quantity < bu :: si :: velocity > avg_speed ( bu :: quantity < bu :: si :: length > d , bu :: quantity < bu :: si :: time > t ) { return d / t ; }
The code always (implicitly) converts incoming
length and
time arguments to the base units of their dimensions. So if the user intends to write the code like:
using kilometer_base_unit = bu :: make_scaled_unit < bu :: si :: length , bu :: scale < 10 , bu :: static_rational < 3 >>>:: type ; using length_kilometer = kilometer_base_unit :: unit_type ; using time_hour = bu :: metric :: hour_base_unit :: unit_type ; using kilometers_per_hour = bu :: divide_typeof_helper < length_kilometer , time_hour >:: type ; BOOST_UNITS_STATIC_CONSTANT ( hours , time_hour ); const auto v = avg_speed ( bu :: quantity < bu :: si :: length > ( 220 * bu :: si :: kilo * bu :: si :: meters ), bu :: quantity < bu :: si :: time > ( 2 * hours )); const bu :: quantity < velocity_kilometers_per_hour > kmph ( v ); std :: cout << kmph . value () << " km/h \n " ;
All the values provided as arguments are first converted to SI base units before the function executes. After the function returns, the result is converted back to the same units as provided by the user for the input arguments. These conversions can significantly slow down the execution of a function, and lead to an increased loss of precision.
For our example, three conversions have to be made. One to convert the length from
to
, one to convert the time from
to
, and one to convert the result
back from
to
. Yet, when considering the units, no conversion
actually has to be made. Simply dividing
by
would suffice.
Even for the case where the result is desired in another unit, the implementation loses on performance and precision:
const auto v = avg_speed ( bu :: quantity < bu :: si :: length > ( 220 * bu :: si :: kilo * bu :: si :: meters ), bu :: quantity < bu :: si :: time > ( 2 * hours )); const bu :: quantity < miles_per_hour > mph ( v ); std :: cout << mph . value () << " mi/h \n " ;
Still three conversions are performed, whereas an optimal implementation would store the result
of
as
without conversion and only convert
to
.
8.1. Template arguments type deduction
Above problem can be solved using function template argument deduction:
template < typename LengthSystem , typename Rep1 , typename TimeSystem , typename Rep2 > constexpr auto avg_speed ( bu :: quantity < bu :: unit < bu :: length_dimension , LengthSystem > , Rep1 > d , bu :: quantity < bu :: unit < bu :: time_dimension , TimeSystem > , Rep2 > t ) { return d / t ; }
This allows us to put requirements on the parameter dimensions without limiting the units allowed. Therefore no conversion before the function call is necessary, reducing conversion overhead and precision loss.
Yet, constraining the return value is a bigger problem. In C++17 it is possible to achieve a constrained return value, but the syntax is not very pretty:
template < typename LengthSystem , typename Rep1 , typename TimeSystem , typename Rep2 > constexpr bu :: quantity < typename bu :: divide_typeof_helper < bu :: unit < bu :: length_dimension , LengthSystem > , bu :: unit < bu :: time_dimension , TimeSystem >>:: type > avg_speed ( bu :: quantity < bu :: unit < bu :: length_dimension , LengthSystem > , Rep1 > d , bu :: quantity < bu :: unit < bu :: time_dimension , TimeSystem > , Rep2 > t ) { return d / t ; }
What is more, the user has to manually reimplement dimensional analysis logic in template metaprogramming land, not actually using the units library which should provide such a functionality.
It is worth noting, that for some libraries we cannot even address the first step for the function template arguments. In the case of [NIC_UNITS] derived units are implemented in terms of base units:
using meter_t = units :: unit_t < units :: unit < std :: ratio < 1 > , units :: category :: length_unit >> ; using kilometer_t = units :: unit_t < units :: unit < std :: ratio < 1000 , 1 > , meter_t > , std :: ratio < 0 , 1 > , std :: ratio < 0 , 1 >>> ;
This makes it impossible to know upfront where
will exist
in a class template instantiation.
8.2. Generic programming with concepts
The answer to constraining templates is again C++20 concepts. With their help the above function can be implemented as:
constexpr units :: Velocity auto avg_speed ( units :: Length auto d , units :: Time auto t ) { return d / t ; }
This gives us the benefit of:
-
working on the user-provided units and values without any intermediate conversions,
-
better error logs (as described in § 7.4 Better errors with C++20 concepts),
-
function template parameter constraints clearly expressed in the function template signature,
-
possibility to constrain not only the function arguments but also its return type without the need to reimplement the body of the function in a template metaprogramming dialect.
With such an approach, the resulting binary generated by the compiler is the same fast (or sometimes even faster) than the one generated for direct usage of fundamental types.
Futhermore it needs to be emphasized, that C++20 concepts are useful not only to constrain function template arguments and their return value but can also be used to constrain the types of user variables:
const units :: Velocity auto speed = avg_speed ( 220 km , 2 h );
If for some reason the function
would no longer return a velocity, the error would
be shown clearly by the compiler, a feature which cannot be provided by C++17 template
metaprogramming.
9. std :: ratio
on steroids
Some of the derived units have really big or small ratios. The difference from the base units is so huge
that it cannot be expressed with
, which is implemented in terms of
.
This makes it really hard to express units like electronvolt (eV) where 1eV = 1.602176634×10−19 J or Dalton where 1 Da = 1.660539040(20)×10−27 kg. Although a custom system of quantities could be a solution, it would only be a workaround as it cannot provide seamless conversion between all possible units.
A better, more flexible solution is needed. One of the possibilities is to extend ratio with one additional parameter:
template < std :: intmax_t Num , std :: intmax_t Den = 1 , std :: intmax_t Exp = 0 > requires ( Den != 0 ) struct new_ratio ;
With such an approach it will be possible to easily address any occurring ratio with a required precision. For example, the conversion rate between one electronvolt and one Joule could be expressed as:
new_ratio < 1602176634 , 1000000000 , - 19 >
10. Other design decisions
10.1. Systems support
From all analysed libraries only [BOOST.UNITS] defines a dedicated type for different systems support. All other libraries either ignore this subject or just provide named units of other systems as scaled units with SI units as a reference. The first approach is too constraining and even the author of [BOOST.UNITS] said:
"My take is that a system is essentially a set of units with linearly independent dimensions and this can be implemented as a convenience on top of the core functionality. Boost.Units started out with a design based solely on systems, but that proved to be too inflexible. We added support for combining individual units, similar to current libraries. However, having both systems and base units supported directly in the core library results in a very convoluted design and is one of the main issues that I wanted to fix in a new library."
On the other hand the second approach seems too limited and naive. It is true that the SI dominates the market but there are domains (i.e. astrology) that commonly use other systems of units (i.e. CGS). For analysis and converting needs quantities of such systems can be expressed as scaled SI units but such an approach will not work for a text output of units symbols.
According to [ISO_80000-1] a system of quantities is a set of quantities together with a set of non-contradictory equations relating those quantities. Those equations are composed from exponents of system’s base quantities. Each base quantity has a base unit which is a measurement unit that is adopted by convention for this base quantity.
It seems that the best way to represent the above in C++ code is to provide a
class template that represents a dimension of a base quantity and is identified with a
pair of an unique compile-time text describing the dimension symbol and a base unit adopted
for this dimension:
template < basic_fixed_string Symbol , Unit U > requires U :: is_named struct base_dimension { static constexpr auto symbol = Symbol ; using base_unit = U ; };
Pair of symbol and unit template parameters form an unique identifier of the base dimension. These identifiers provide total ordering of exponents of base dimensions in a derived dimension.
The SI physical units system defines 7 base dimensions:
namespace units :: si { struct dim_length : base_dimension < "L" , metre > {}; struct dim_mass : base_dimension < "M" , kilogram > {}; struct dim_time : base_dimension < "T" , second > {}; struct dim_electric_current : base_dimension < "I" , ampere > {}; struct dim_thermodynamic_temperature : base_dimension < "Θ" , kelvin > {}; struct dim_substance : base_dimension < "N" , mole > {}; struct dim_luminous_intensity : base_dimension < "J" , candela > {}; }
All other derived quantities of SI are composed from those.
There are two reasons why a
gets a unit as its template parameter. First,
the base unit is needed for the text output of unnamed derived units. Second, by providing
a different unit for the same physical dimension we can define a base quantity of
another system. For example CGS definitions may look as follows:
namespace units :: cgs { struct dim_length : base_dimension < "L" , centimetre > {}; struct dim_mass : base_dimension < "M" , gram > {}; using si :: dim_time ; }
Equivalent base dimensions in different systems have the same symbol identifiers and get units
from the same hierarchy (with the same reference in
). Thanks to that we have
the ability to explicitly cast quantities of the same dimension from different systems or
even mix them in one
definition.
Another important point to notice is that the systems of units are not closed (classes) but open (namespaces) and can be easily extended, or its content can be partially/fully imported to other systems.
10.2. Dimensionless quantities
Some quantities of dimension one are defined as ratios of two quantities of the same kind. The coherent derived unit is the number one, symbol 1. For example: plane angle, solid angle, refractive index, relative permeability, mass fraction, friction factor, Mach number, etc.
There are two implementation possibilities here:
Option 1
auto q1 = length < metre , double > () / length < metre , double > (); static_assert ( std :: is_same_v < q1 , double > );
Option 2
auto q1 = length < metre , double > () / length < metre , double > (); static_assert ( std :: is_same_v < q1 :: dimension , dimension <>> );
"Option 2" provides a false impression of strong type safety. We have something that looks like a strong type but is actually as dumb as a scalar. In most of the libraries that follow such a way this type has a special semantics (provided either via a partial specialization or SFINAE). For example in [BOOST.UNITS]:
"Because dimensionless quantities have no associated units, they behave as normal scalars, and allow implicit conversion to and from the underlying value type or types that are convertible to/from that value type."
It was decided to continue with an "Option 1". "If you are as dumb as a scalar, be a scalar".
If a user gets a
from an
he/she knows exactly what to expect. If it
is some strange quantity-ish type it is exactly the opposite. It is also easier to standardize
and implement.
10.3. No runtime-specified conversions
It might be tempting to provide runtime conversion factors between units of the same quantity i.e. to provide a solution to convert between different currencies. From all analysed libraries only [BOOST.UNITS] supports such a functionality. However, such an approach complicates the design and bends it towards solutions that are not strictly related to physical quantities.
There was a really strong consensus during the Belfast meeting discussion to not provide such a functionality which also corresponds to the authors private opinion on this subject.
10.4. No initial support for quantity kinds
According to [ISO_80000-1] quantities of the same kind within a given system of quantities have the same quantity dimension but quantities of the same dimension may not necessarily be of the same kind. A good example here might be width, depth, and height of the package. All of those quantities are of length dimension but it is questionable if all quantity arithmetic operations (i.e. addition) should be allowed between their values (namely, should we be allowed to add width to height).
Such abstractions, limitations, and operations can and should be provided by the software layers above the generic purpose physical units library. It is a domain-specific knowledge what are the quantity kinds and which operations should be allowed between them.
The author’s opinion was backed up by the poll in Belfast where the room felt strongly against prioritizing quantity kinds in the initial release of the library.
10.5. Text output
There is a popular demand for the physical units library is to be able to automatically
print quantity unit symbols. Some of the analysed libraries already provide such
a support but none of them supports
facility. In Belfast, SG16 decided that
we should scope mainly on this facility and leave the support for
to the
author’s discretion.
During the same meeting in Belfast, SG16 was really pushing towards localizations support.
However, it was decided that it should be done only after the initial library version is
done and when an alternative to
is introduced in C++.
11. Extensibility
The units library should be designed in a way that allows users to easily extend it with their own systems, units, derived, or even base dimensions. The C++ Standard Library will most likely decide to ship with a support for "just" physical units with possible extension to digital information dimensions and their units. This should not limit users to the units and quantities provided by library engine, but address all their needs in their specific domains.
The most important points that have to be provided by such C++ Standard library engine in order to provide good extensibility are:
-
The library has to be extendible with new systems, units, derived, and base quantities, interacting with the existing ones.
-
The user-defined entities have to provide the same user experience as built-in ones.
-
Extension shall be possible without preprocessor macros in the user interface.
-
Extensions to the C++ Standard library engine created by two independent vendors shall not collide with each other as long as they address separate domains.
12. Easy to use and hard to abuse
Users complain about the complexity of existing solutions. For example, Boost.Units users have to:
-
include a lot of specific header files,
-
define a lot of types by themselves (see § 8 Limiting intermediate quantity value conversions),
-
fight with compilation errors (see § 7 Improving user experience) and debugging,
-
define custom systems to workaround intermediate conversions issues (see § 8.2 Generic programming with concepts and § 9 std::ratio on steroids)
-
learn lots of library specific behaviors and their side effects (i.e. lack of implicit conversions between units even when it is provable in compile time that such a translation is non-truncating like km -> m),
Most of those issues can be solved during the design time. We should strive to provide:
-
Behavior similar to
as it proved to be a good design and the user base already got used to that.std :: chrono -
Clear responsibility of each type (unit -> base_dimension -> exp -> derived_dimension -> quantity).
-
Ease to extend with custom dimensions or units.
-
Ease to understand error messages and a good debugging experience thanks to downcast facility and concepts.
-
A basic set of prefixes, units, quantities, constants, and concepts.
13. Design principles
The basic design principles that should be used to implement a physical units library for C++ are:
-
Safety and performance:
-
strong types
-
only safe implicit conversions should be allowed
-
compile-time safety and verification wherever possible (break at compile time, not at runtime)
-
constexpr all the things
-
as fast or even faster than working with fundamental types
-
-
The best possible user experience:
-
interfaces embraced with clear concepts and contracts
-
user friendly compiler errors
-
good debugging experience
-
-
No macros in the user interface.
-
Easy extensibility.
-
No external dependencies.
-
Possibility to be standardized as a freestanding part of the C++ Standard Library.
-
Batteries included:
-
provide basic prefixes, units, quantities, constants, and concepts
-
non-experts should easily be able to achieve simple calculations
-
14. Open questions
14.1. UDLs vs constants to create quantities
There are two different ways to create quantities. The first one is
-like where
for each unit we define an UDL:
namespace units :: si :: inline literals { // m constexpr auto operator "" m ( unsigned long long l ) { return length < metre , std :: int64_t > ( l ); } constexpr auto operator "" m ( long double l ) { return length < metre , long double > ( l ); } // cm constexpr auto operator "" cm ( unsigned long long l ) { return length < centimetre , std :: int64_t > ( l ); } constexpr auto operator "" cm ( long double l ) { return length < centimetre , long double > ( l ); } }
The second possibility here is to use constants:
namespace units :: si { inline constexpr length < metre , std :: int64_t > m ( 1 ); inline constexpr length < centimetre , std :: int64_t > cm ( 1 ); }
Initially, we decided to be consistent with
but the more experience we get the
more problems we see with such an approach:
-
UDLs are only for compile-time known values. Currently with UDLs:
using namespace units :: si ; auto v1 = 120 km / 2 h ; auto v2 = length < kilometre > ( length ) / time < hour > ( duration ); with constants those 2 cases would look like:
using namespace units :: si ; auto v1 = 120 * km / ( 2 * h ); auto v2 = length * km / duration * h ; Constants treat both cases in a unified way. It is also worth to notice that we work mostly with runtime variables and compile-time known values appear only in physical constants and unit tests.
A tricky case here for constants that we cannot fell into
is always equivalent to2 h
. In such a case2 * h
would be invalid. It has to either be spelled asauto v1 = 120 * km / 2 * h ;
orauto v1 = 120 * km / ( 2 * h );
. This is probably the biggest issue with this syntax.auto v1 = 120 * km / 2 / h ; -
UDLs for some units may be impossible to achieve in C++. We already found issues with
(farad),F
(joule),J
(watt),W
(kelvin),K
(day),d
orl
(litre),L
,erg
. It is probably still not the complete list here. All of those problems originated from the fact that those numeric symbols are already used in literals (sometimes compiler extensions but still). We can fix some of those cases with additional wording but probably some of them will remain broken and we will need to invent workarounds. None of those issues affect constants.ergps -
UDLs cannot be disambiguated with the namespace name:
using namespace si ; auto d = 1 cm ; // quantity<si::dim_length, si::centimetre> using namespace cgs ; auto d = 1 cm ; // quantity<cgs::dim_length, si::centimetre> using namespace si ; using namespace cgs ; auto d = 1 cm ; // FAILS TO COMPILE With constants it is simple:
using namespace si ; using namespace cgs ; auto d1 = 1 * si :: cm ; // quantity<si::dim_length, si::centimetre> auto d2 = 1 * cgs :: cm ; // quantity<cgs::dim_length, si::centimetre>
Should we remain consistent with
and use UDLs which are considered a modern C++
way to create an instance of a concrete type, or should we reach for pre-C++11 ways of doing
it which actually may prove better in this particular case?
14.2. Affine types
One of the most critical aspects of the physical units library is to understand what a quantity is? An absolute or a relative value? For most dimensions only relative values have sense. For example:
-
Where are absolute 123 meters?
-
If I am sitting in a moving train, is my velocity == 0?
-
Is my velocity == 0 when the train stops?
However, for some quantities like temperature, absolute values are really needed.
For example, how much is
? Is it
or
or
? Yes, the
repeated value of
is not an error here ;-) Actually, all of the answers are right:
-
Two (absolute) temperatures:
0 ℃ + 0 ℃ = 273.15 K + 273.15 K = 546.30 K = 273.15 ℃
-
An (absolute) temperature and a (relative) temperature interval:
0 ℃ + 0 ℃ = 273.15 K + 0 K = 273.15 K = 0 ℃
-
Two (relative) temperature intervals:
0 ℃ + 0 ℃ = 0 K + 0 K = 0 K = 0 ℃
As described above, it is a complex and a pretty hard problem. The average user of the library
will probably not be able to distinguish between different kinds of quantities. This is
why it was decided that a
type will model only relative quantity values and
specifically for temperatures to provide support only for kelvins.
Later on as an extension we can consider one of the two alternatives:
-
Provide additional type called
that will model absolute quantity value semantics. Only then we suggest to provide support for other temperature units.quantity_point -
Provide verbose non-member utility functions for conversions between different kinds of temperature values and their units.
14.3. Interoperability with std :: chrono :: duration
One of the most challenging problems to solve in the physical units library will be
the interoperability with
.
is an excellent library
and has a wide adoption in the industry right now. However it has also some issues that
make its design not suitable for a general purpose units library framework:
-
It addresses only one of many dimensions, namely time. There is no possibility to extend it with other dimensions support.
-
class template needs a few more member functions to provide support for conversions between different dimensions.quantity -
SG6 members raised an issue with
returningstd :: chrono :: duration
from most of the arithmetic operators. This does note play well with custom representation types that return different type in case of multiplication and different in case of division operation.std :: common_type_t < Rep1 , Rep2 > -
is not able to handle large prefixes required by some units (more information in § 9 std::ratio on steroids).std :: ratio
Because of the above issues we cannot just use
design as it is right
now and use it for physical units implementation or even as a representation of only time quantity. There are however, a few possibilities here to provide interoperability between
the types:
-
Provide built-in conversion facility that among others can be used to allow conversion between
andstd :: chrono :: duration
.std :: units :: quantity -
Provide non-member functions to convert and compare between those two types.
-
Ignore
, do not provide any conversion utilities in the standard library and leave it for the user.std :: chrono :: duration
15. Impact on the Standard
The library would be mostly a pure addition to the C++ Standard Library with the following potential exceptions:
-
It is unclear how to provide interoperability with the
(more information in § 14.3 Interoperability with std::chrono::duration).std :: chrono :: duration -
will be a different type with the similar semantics tostd :: units :: ratio
(more information in § 9 std::ratio on steroids). However, if we decide C++23 to be an ABI breaking release we could updatestd :: ratio
with an additional template parameter.std :: ratio
16. Implementation Experience
The author of this document implemented
[MP-UNITS] library, where he tested
different ideas and proved the implementability of the features described in the paper.
16.1. Usage example
#include <units/physical/si/velocity.h>#include <units/physical/international/velocity.h>#include <iostream>#include <cassert>constexpr units :: Velocity auto avg_speed ( units :: Length auto d , units :: Time auto t ) { return d / t ; } void test1 () { using namespace units :: si :: literals ; const auto v = avg_speed ( 220 km , 2 h ); assert ( v . count () == 110 ); // passes assert ( v == 110 kmph ); // passes std :: cout << v << '\n' ; // prints "110 km/h" } void test2 () { using namespace units ; using namespace units :: international :: literals ; const auto v = avg_speed ( si :: length < international :: mile > ( 140 ), si :: time < si :: hour > ( 2 )); assert ( v . count () == 70 ); // passes assert ( v == 70 mph ); // passes std :: cout << v << '\n' ; // prints "70 mi/h" }
16.2. Design
The library framework consists of a few concepts: quantities, units, dimensions, and their exponents. From the user’s point of view, the most important is a quantity.
Quantity is a precise amount of a unit for a specified dimension with a specific representation:
units :: si :: length < units :: si :: kilometre , double > d1 ( 123 ); auto d2 = 123 km ; // units::quantity<units::si::dim_length, units::si::kilometre, std::int64_t>
There are C++ concepts provided for each such quantity type:
template < typename T > concept Length = physical :: QuantityOf < T , physical :: dim_length > ;
With these concepts, we can easily write a function template:
constexpr units :: Velocity auto avg_speed ( units :: Length auto d , units :: Time auto t ) { return d / t ; }
This template function can be used in the following way:
using namespace units ; const si :: length < si :: kilometre > d ( 220 ); const si :: time < si :: hour > t ( 2 ); const Velocity auto kmph = quantity_cast < si :: kilometre_per_hour > ( avg_speed ( d , t )); std :: cout << kmph << " \n " ; const Velocity auto speed = avg_speed ( 140 mi , 2 h ); assert ( speed . count () == 70 ); std :: cout << quantity_cast < international :: mile_per_hour > ( speed ) << " \n " ;
This guarantees that no intermediate conversions are being made, and the output binary is as
effective as implementing the function with
s.
Additionally, thanks to the extensive usage of the C++ concepts and the downcasting facility, the library provides an excellent user experience. The error message for type aliases would look like:
[with D = units::quantity<units::derived_dimension_base<units::exp<units::si::dim_length, 1, 1>, units::exp<units::si::dim_time, 1, -1> >, units::scaled_unit<units::ratio<5, 18>, units::si::metre_per_second>, double>]
Yet, thanks to downcast facility, the actual error message is:
[with D = units::quantity<units::si::dim_velocity, units::si::kilometre_per_hour, double>]
The breakpoint in the debugger became readable as well:
Breakpoint 1, avg_speed<units::quantity<units::si::length, units::si::kilometre, double>, units::quantity<units::si::time, units::si::hour, double> > (d=..., t=...) at velocity.cpp:31 31 return d / t;
Moreover, it is really easy to extend the library with custom units, derived units, and
base dimensions. A great example of a adding a whole new system can be a
system in
the library which adds support for digital information quantities. In summary it adds:
-
New prefix type and its prefixes:
namespace units :: data { struct prefix : prefix_type {}; struct kibi : units :: prefix < kibi , prefix , "Ki" , ratio < 1 ’024 >> {}; struct mebi : units :: prefix < mebi , prefix , "Mi" , ratio < 1 ’04 8 ’576 >> {}; }
-
New units for
:information
namespace units :: data { struct bit : named_unit < bit , "b" , prefix > {}; struct kibibit : prefixed_unit < kibibit , kibi , bit > {}; struct byte : named_scaled_unit < byte , "B" , prefix , ratio < 8 > , bit > {}; struct kibibyte : prefixed_unit < kibibyte , kibi , byte > {}; }
-
New base dimension, its concept, and quantity alias:
namespace units :: data { struct dim_information : base_dimension < "information" , bit > {}; template < typename T > concept Information = QuantityOf < T , dim_information > ; template < Unit U , Scalar Rep = double > using information = quantity < dim_information , U , Rep > ; }
-
UDLs for new units
namespace units :: data :: inline literals { // bits constexpr auto operator "" b ( unsigned long long l ) { return information < bit , std :: int64_t > ( l ); } constexpr auto operator "" Kib ( unsigned long long l ) { return information < kibibit , std :: int64_t > ( l ); } // bytes constexpr auto operator "" B ( unsigned long long l ) { return information < byte , std :: int64_t > ( l ); } constexpr auto operator "" KiB ( unsigned long long l ) { return information < kibibyte , std :: int64_t > ( l ); } }
-
A new
derived dimension, its units, concept, quantity helper, and UDLsbitrate
namespace units :: data { struct bit_per_second : unit < bit_per_second > {}; struct dim_bitrate : derived_dimension < dim_bitrate , bit_per_second , exp < dim_information , 1 > , exp < si :: dim_time , - 1 >> {}; struct kibibit_per_second : deduced_unit < kibibit_per_second , dim_bitrate , kibibit , si :: second > {}; template < typename T > concept Bitrate = QuantityOf < T , dim_bitrate > ; template < Unit U , Scalar Rep = double > using bitrate = quantity < dim_bitrate , U , Rep > ; inline namespace literals { // bits constexpr auto operator "" _bps ( unsigned long long l ) { return bitrate < bit_per_second , std :: int64_t > ( l ); } constexpr auto operator "" _Kibps ( unsigned long long l ) { return bitrate < kibibit_per_second , std :: int64_t > ( l ); } } }
17. Polls
17.1. LEWG
-
Do we want a physical units library in the C++ standard?
-
Do we prefer UDL or constants to create quantities ([[#udls_vs_constants ]])?
-
Do we like the concept-based approach to prevent truncation and intermediate conversions (§ 8.2 Generic programming with concepts)?
-
Do we like a downcasting facility and would like to standardize it as a part of
(§ 7.2 Downcasting facility)?std -
What about
(§ 14.3 Interoperability with std::chrono::duration)?std :: chrono :: duration
17.2. SG6
-
Do we want to have built-in support for digital information dimensions and its prefixes in the initial version of the library?
-
Should we provide built-in support for some off-system units or limit to SI units only?
-
Do we want to require explicit representation casts between the same units of the same dimension, or do we allow
-like implicit conversions (floating-point representation and non-truncating integer conversions)?chrono -
Do we want to require explicit unit casts between different units of the same dimension, or do we allow
-like implicit conversions (implicitly convert kilometre to metre)?chrono -
Do we agree with Kelvins only support for temperature and verbose conversion functions for other units and absolute temperatures? Should affine types be provided (§ 14.2 Affine types)?
18. Acknowledgments
Special thanks and recognition goes to Epam Systems for supporting my membership in the ISO C++ Committee and the production of this proposal.
I would also like to thank Jan A. Sende for his contributions to the
library and
this document.