Jump to Table of Contents Collapse Sidebar

P1935R1
A C++ Approach to Physical Units

Published Proposal,

This version:
https://mpusz.github.io/wg21-papers/papers/1935_a_cpp_approach_to_physical_units.html
Author:
Mateusz Pusz (Epam Systems)
Audience:
LEWG, SG6, SG16, SG18
Project:
ISO/IEC JTC1/SC22/WG21 14882: Programming Language — C++
Source:
github.com/mpusz/wg21_papers/blob/master/src/1935_a_cpp_approach_to_physical_units.bs

Abstract

This document starts the discussion about the Physical Units support for the C++ Standard Library. The reader will find here the rationale for such a library. After that comes the review and comparison of current solutions on the market followed by the analysis of the problems related to their usage and user experience. The rest of the document describes solutions and techniques that can be used to mitigate those issues. All of them were implemented and tested by the author in the mp-units library.

1. Revision History

1.1. r0 ➡ r1

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:

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 double. 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 double 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 std::chrono 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. std::chrono is the ultimate answer. So let us try to get std::units 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(10km / 2 == 5km);

// unit conversions
static_assert(1h == 3600s);
static_assert(1km + 1m == 1001m);

// dimension conversions
static_assert(1km / 1s == 1000mps);
static_assert(2kmph * 2h == 4km);
static_assert(2km / 2kmph == 1h);

static_assert(1000 / 1s == 1kHz);

static_assert(10km / 5km == 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::kilometre_per_hour>(speed) << '\n';

3.3. Scope

Although there is a public demand for a generic units library that could handle any units and dimensions, the author suggests scoping the Committee efforts only on the physical and possibly computer science (i.e. bit, byte, bitrate) units first. The library should be designed with easy extensibility in mind so anyone needing a new base or derived dimensions (i.e. coffee/milk/water/sugar 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

kind of quantity, kind

system of quantities, system

base quantity

derived quantity

International System of Quantities (ISQ)

dimension of a quantity, quantity dimension, dimension

quantity of dimension one, dimensionless quantity

unit of measurement, measurement unit, unit

base unit

derived unit

coherent derived unit

system of units

coherent system of units

off-system measurement unit, off-system unit

International System of Units (SI)

multiple of a unit

submultiple of a unit

quantity value, value of a quantity, value

4.2. Other definitions

base dimension

derived dimension

reduced dimension

A derived dimension in which:

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.

{This chapter is incomplete and will be filled in D1935R1 that should be available as a draft on the LEWG Wiki before Belfast meeting}

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"
}

Compiler Explorer

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"
}

Compiler Explorer

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"
}

Compiler Explorer

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 typelists of base dimensions must be created by using the dim class to encapsulate pairs of base dimensions and static_rational exponents. The make_dimension_list class acts as a wrapper to ensure that the resulting type is in the form of a reduced 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 base_dimension:

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 BOOST_UNITS_STATIC_CONSTANT 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 base_unit_info 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 kilogram_base_unit and second_base_unit.

It is possible to define a base unit as being a multiple of another base unit. For example, the way that kilogram_base_unit 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 make_scaled_unit 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 quantity 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 pow<R> and root<R> 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 BOOST_UNITS_DEFAULT_CONVERSION 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:

There are two distinct types of systems that can be envisioned:

  1. 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.

  2. 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 m ft 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: H = (r/C)^2 where H is the radar beam height in feet and r is the radar range in nautical miles. In order to enforce dimensional correctness of this equation, the constant, C, must be expressed in nautical miles per foot^(1/2), mixing two distinct base units of length.

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:

  1. We are unable to pass arguments to avg_speed 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).

  2. Because of above we also do not get the result in the unit we would like. This it why the first assert fails.

  3. There is no possibility to cast returned quantity to the unit that we would like to use for printing.

  4. 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 length_d and mass_d 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 (meter, kilogram, ...):

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 yocto to yotta. Thus it is possible to write quantity literals such as 1_ns and 42.195_km.

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"
}

Compiler Explorer

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. units::category::length_unit):

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 T 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 (units.h) 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 Radian and Byte.

Units in the library are defined in terms of:

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 (m * m^-1), 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 unit_t class, and have the form [unitname]_t, e.g. meter_t or radian_t.

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 static_assert to report conversion errors rather than to relay on an overload resolution process (and SFINAE). The side effects of this are:

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
}

Compiler Explorer

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 quantity 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. 110_kilo * metre / hour). 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:

5.5.2. Design

The unit 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 Dimension and Prefix are sorted type lists. Representing a prefix as a type list is unique to this library and is meant to address limited range of std::ratio.

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 quantity 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:

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 no yes (bytes, radians) yes
C++ version C++20 C++98 + constexpr C++11 C++14 C++14
Base dimension id string integer index on template parameter list index on template parameter list string
Dimension type (length) alias to type list (length_dimension) alias to type list (length_d) namespace (length) alias to type list (length_t)
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 (metre) alias + constant (si::length + si::meter) value (meter) alias (length::meter_t) alias (metre_t)
UDLs yes no some yes yes (long form only i.e. _metre)
Composable Units no prefix only (kilo * metre) prefix only (kilo * metre) no yes (kilo * metre)
Predefined scaled unit types some no some all no
Scaled units type + UDL (kilometre + km) predefined values or multiplied with a prefix (si::kilo * si::meter) value + UDL (mile + _km) type + UDL (length::kilometer_t + _km) no
Meter vs metre metre both both meter metre
Singular vs plural singular (metre) both (meter + meters) singular both (length::meter_t + length::meters_t) singular (metre)
Quantity type (quantity<metre> q(2);) type (quantity<si::length> q(2 * si::meter);) type (quantity<speed_d> speed = 45_km / hour;) type (length::meter_t d(220);) type (quantity<metre> q(2);)
Literal instance UDL (123m) Number * static constant (123 * si::meters) UDL (1_m) UDL (123_m) UDL (1_metre)
Variable instance constructor (quantity<metre>(v)) Variable * static constant (d * si::meters) Variable * static constant (d * kilo * meter) constructor (length::meter_t(v)) constructor (quantity<metre>(v))
Any representation yes yes yes no (macro to set the default type) yes (macro default of double)
Quantity template arguments type deduction yes yes no no no
System support 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 quantity_cast quantity_cast yes no simple_cast (constexpr)/unit_cast
Runtime conversion factors no yes no no no
Temperature support Kelvins only + conversion functions Kelvins in SI + other in dedicated systems relative values only absolute values only yes
String output yes yes yes yes 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
<cmath> support TBD yes no yes yes
<chrono> support ??? no no yes yes
Affine types ??? yes (absolute<Q>) no no yes (quantity, quantity_point)
Prefix representation ratio scale<10, static_rational<exponent>> long double ratio type list
Physical/Mathematical constants TBD yes limited limited all
Dimensionless quantity ??? 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:

  1. Bad user experience caused by hard to understand and analyze compile-time errors and poor debugging experience (addressed by § 7 Improving user experience).

  2. 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).

  3. Poor support for really large or small unit ratios (i.e. eV) (addressed by § 9 std::ratio on steroids).

  4. Impossibility or hard extensibility of the library with new base quantities (addressed by § 10 Extensibility).

  5. Too high entry bar (e.g. Boost.Units is claimed to require expertise in both C++ and dimensional analysis) (addressed by § 11 Easy to use and hard to abuse).

  6. Safety and security connected problems with the usage of an external 3rd party library for production purposes (addresed 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::unit<units::dimension<units::exp<units::base_dim_length, 1>,
units::exp<units::base_dim_time, -1> > >, std::ratio<1> >, double>]

into:

[with T = units::quantity<units::metre_per_second, 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 : dimension<exp<base_dim_length, 1>, exp<base_dim_time, -1>>;
struct metre_per_second : unit<velocity, "m/s", std::ratio<1>>;

In the above example, velocity and metre_per_second are the downcasting targets (child classes), and specific dimension and unit 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 base_type = BaseType;
  friend auto downcast_guide(downcast_base);
};

units::downcast_base is a class that implements the CRTP idiom, marks the base of the downcasting facility with a base_type member type, and provides a declaration of the downcasting ADL friendly (Hidden Friend) entry point member function downcast_guide. 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 downcast_helper and will return a target type of the downcasting operation there.

template<typename T>
concept Downcastable =
    requires {
      typename T::base_type;
    } &&
    std::derived_from<T, downcast_base<typename T::base_type>>;

units::Downcastable is a concept that verifies if a type implements and can be used in a downcasting facility.

template<typename Target, Downcastable T>
struct downcast_helper : T {
  friend auto downcast_guide(typename downcast_helper::downcast_base) { return Target(); }
};

units::downcast_helper is another CRTP class template that provides the implementation of a non-member friend function of the downcast_base class template, which defines the target type of a downcasting operation. It is used in the following way to define dimension and unit types in the library:

template<typename Child, Exponent... Es>
struct derived_dimension : downcast_helper<Child, typename detail::make_dimension<Es...>::type> {};
template<typename Child, basic_fixed_string Symbol, Dimension D, typename PrefixType = no_prefix>
struct coherent_derived_unit : downcast_helper<Child, unit<D, ratio<1>>> {
  static constexpr auto symbol = Symbol;
  using prefix_type = PrefixType;
};

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 velocity : derived_dimension<velocity, exp<base_dim_length, 1>, exp<base_dim_time, -1>> {};
struct metre_per_second : coherent_derived_unit<metre_per_second, "m/s", velocity> {};

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_target = decltype(detail::downcast_target_impl<T>());

units::downcast_target is used to obtain the target type of the downcasting operation registered for a given instantiation in a base type.

For example, to determine a downcasted type of a quantity multiplication, the following can be done:

using dim = dimension_multiply<typename U1::dimension, typename U2::dimension>;
using ratio = ratio_multiply<typename U1::ratio, typename U2::ratio>;
using common_rep = decltype(lhs.count() * rhs.count());
using ret = quantity<downcast_target<unit<dim, ratio>>, common_rep>;

detail::downcast_target_impl checks if a downcasting target is registered for the specific base class. If registered, detail::downcast_target_impl 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 typename T as their parameter, not placing any constratints on the actual type. In C++17 placing such constraints is possible thanks to SFINAE and helpers like std::enable_if or std::void_t. 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, gcc with experimental Concepts TS support generates the following message:

example.cpp: In instantiation of ‘constexpr units::Velocity avg_speed(D, T)
    [with D = units::quantity<units::kilometre>; T = units::quantity<units::hour>]’:
example.cpp:49:49:   required from here
example.cpp:34:14: error: placeholder constraints not satisfied
    34 |   return d * t;
       |              ^
include/units/dimensions/velocity.h:34:16: note: within ‘template<class T> concept units::Velocity<T>
    [with T = units::quantity<units::unit<units::dimension<units::exp<units::base_dim_length, 1, 1>,
                units::exp<units::base_dim_time, 1, 1> >, units::ratio<3600000, 1> >, double>]’
    34 |   concept Velocity = Quantity<T> && std::same_as<typename T::dimension, velocity>;
       |           ^~~~~~~~
include/stl2/detail/concepts/core.hpp:37:15: note: within ‘template<class T, class U> concept std::same_as<T, U>
    [with T = units::dimension<units::exp<units::base_dim_length, 1, 1>, units::exp<units::base_dim_time, 1, 1> >;
          U = units::velocity]’
    37 |  META_CONCEPT same_as = meta::Same<T, U> && meta::Same<U, T>;
       |               ^~~~~~~
include/meta/meta_fwd.hpp:224:18: note: within ‘template<class T, class U> concept meta::Same<T, U>
    [with T = units::dimension<units::exp<units::base_dim_length, 1, 1>, units::exp<units::base_dim_time, 1, 1> >;
          U = units::velocity]’
   224 |     META_CONCEPT Same =
       |                  ^~~~
include/meta/meta_fwd.hpp:224:18: note: ‘meta::detail::barrier’ evaluated to false
include/meta/meta_fwd.hpp:224:18: note: within ‘template<class T, class U> concept meta::Same<T, U>
    [with T = units::velocity;
          U = units::dimension<units::exp<units::base_dim_length, 1, 1>, units::exp<units::base_dim_time, 1, 1> >]’
include/meta/meta_fwd.hpp:224:18: note: ‘meta::detail::barrier’ evaluated to false

While still being a little verbose, this is a big improvement to the page-long instantation 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 occured. With concept suppport 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 d length and t 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 220km to 220000m, one to convert the time from 2h to 7200s, and one to convert the result back from 30.5555...m/s to 110km/s. Yet, when considering the units, no conversion actually has to be made. Simply dividing 220 by 2 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 220km/2h as 110km/h without conversion and only convert 110km/h to 68.35mi/h.

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 units::category::length_unit 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:

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.

Additionally, concept usage relieves us from the need to implement a system of quantities, which in other libraries needs to be defined to fix a custom base unit to a specific dimension. In these libraries, defining such a unit system is a workaround for constraining template function parameters and limiting the number of intermediate conversions.

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 avg_speed 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 std::ratio, which is implemented in terms of std::intmax_t.

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 possiblities might be to redefine 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 occuring 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. Extensibility

The units library should be designed in a way that allows users to easily extend it with their own 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:

11. Easy to use and hard to abuse

Users complain about the complexity of existing solutions. For example, Boost.Units users have to:

Most of those issues can be solved during the design time. We should strive to provide:

  1. Behavior similar to std::chrono as it proved to be a good design and the user base already got used to that.

  2. Clear responsibility of each type (base_dimension -> exp -> dimension -> unit -> quantity).

  3. Ease to extend with custom dimensions or units.

  4. Ease to understand error messages and a good debugging experience thanks to downcast facility and concepts.

  5. No dedicated abstraction for systems that would complicate implementation and reasoning about the library engine and functionality (at least until future users will not provide solid requirements and use cases for such an entity).

  6. A basic set of prefixes, units, quantities, constants, and concepts.

12. Design principles

The basic design principles that should be used to implement a physical units library for C++ are:

  1. 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

  2. The best possible user experience:

    • interfaces embraced with clear concepts and contracts

    • user friendly compiler errors

    • good debugging experience

  3. No macros in the user interface.

  4. Easy extensibility.

  5. No external dependencies.

  6. Possibility to be standardized as a freestanding part of the C++ Standard Library.

  7. Batteries included:

    • provide basic prefixes, units, quantities, constants, and concepts

    • non-experts should easily be able to achieve simple calculations

13. Open questions

13.1. How to represent SI prefixes and derived units?

There are at least 3 ways to represent derived units:

  1. Provide a new strong type and an UDL for each unit

  2. Define the type only for a coherent derived unit and use multiplier syntax to obtain more derived units

  3. Mixed approach using strong types, NTTPs, and variable templates

Starting with the first case. Each derived unit gets its own type and an UDL. With such an approach we can easily write:

using units::literals;

const auto d1 = 123km;
const auto d2 = units::quantity<units::kilometre>(123);

const auto v1 = 123kmph;
const auto v2 = units::quantity<units::kilometre_per_hour>(123);

The good parts here are:

The drawbacks of such a solution are:

The second case assumes that each dimension will get only a predefined coherent derived unit and the rest of the derived units will be either created with a multiplier syntax or defined by the user:

namespace bu = boost::units;

const auto d1 = 123k * bu::si::meters;  // no an actual Boost.Units syntax
const auto d2 = 123 * bu::si::kilo * bu::si::meters;

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 velocity_kilometers_per_hour = bu::divide_typeof_helper<length_kilometer, time_hour>::type;
BOOST_UNITS_STATIC_CONSTANT(kilometers_per_hour, velocity_kilometers_per_hour);

// const auto v1 = ???
const auto v2 = 123 * kilometers_per_hour;

The good parts here are:

The drawbacks of such a solution are:

The third approach is using a mix of several language features including strong types, Non Type Template Parameters (NTTP), and UDLs. With such an approach we can end up with a variety of syntaxes. Please note that the long list below is only to list all of the possibilities in this design space and we do not propose anything like this for now. We can easily forbid any or all of the following syntaxes:

inline constexpr auto kilometre = kilo*metre;
inline constexpr auto km = kilometre;

namespace literals {
  constexpr auto operator ""km(unsigned long long l) { return quantity<km, std::int64_t>(l); }
  constexpr auto operator ""km(long double l) { return quantity<km, long double>(l); }
}

const auto d1  = quantity<kilo*metre>(123);
const auto d2  = quantity<kilometre>(123);
const auto d3  = quantity<k*metre>(123);
const auto d4  = quantity<k*m>(123);
const auto d5  = quantity<km>(123);
const auto d6  = kilo(123)*metre;
const auto d7  = kilometre(123);
const auto d8  = kilo*metre(123);
const auto d9  = kilometre(123);
const auto d10 = 1000*metre(123);
const auto d11 = metre(123000);
const auto d12 = 123k*m;
const auto d13 = 123km;
const auto d14 = k*123m;
const auto d15 = 123kilo*metres;
const auto d16 = (km)(123);

const auto v1  = quantity<kilo*metre/hour>(123);
const auto v2  = quantity<kilometre/hour>(123);
const auto v3  = quantity<k*m/h>(123);
const auto v4  = quantity<km/h>(123);
const auto v5  = kilo * metre(123) / hour(1);
const auto v6  = kilo * metre(123) / hour();
const auto v7  = kilometre(123) / hour;
const auto v8  = k*m(123)/h;
const auto v9  = km(123)/h;
const auto v10 = (km/h)(123);
const auto v11 = 123km/h;

All of the above variables for length and velocity are respectively of the same unit and contain the same value.

The good parts here are:

The drawbacks of such a solution are:

Please note that 2nd and 3rd case may look tempting because nice examples where used. It can get worse for a lot of dimensions:

while the same dimensions with the first approach looks as follows:

and provide dowcasting support at the same time which will provide exactly the same experience for the end user in a compilation log or the compiler to the one that developer has while implementing the library code.

13.2. NTTP usage

There are a few points in the physical units domain design that could benefit from Non-Type Template Parameters usage. One of the most apparent cases here is ratio. A classical implementation of such a class template looks like this:

template<intmax_t Num, intmax_t Den = 1>
struct ratio {
  static constexpr intmax_t num = Num * static_sign<Den>::value / static_gcd<Num, Den>::value;
  static constexpr intmax_t den = static_abs<Den>::value / static_gcd<Num, Den>::value;
  using type = ratio<num, den>;
};

Besides, it provides a few utilities to do operations on such types:

namespace detail {
  template<typename R1, typename R2>
  struct ratio_multiply_impl {
  private:
    static constexpr intmax_t gcd1 = static_gcd<R1::num, R2::den>::value;
    static constexpr intmax_t gcd2 = static_gcd<R2::num, R1::den>::value;
  public:
    using type = ratio<safe_multiply<(R1::num / gcd1), (R2::num / gcd2)>::value,
                       safe_multiply<(R1::den / gcd2), (R2::den / gcd1)>::value>;
    static constexpr intmax_t num = type::num;
    static constexpr intmax_t den = type::den;
  };
}
template<typename R1, typename R2>
using ratio_multiply = detail::ratio_multiply_impl<R1, R2>::type;

Usage examples of such an approach looks as follows:

struct yard : derived_unit<yard, "yd", length, ratio<9144, 10000>> {};
struct foot : derived_unit<foot, "ft", length, ratio_multiply<ratio<1, 3>, yard::ratio>> {};
struct inch : derived_unit<inch, "in", length, ratio_multiply<ratio<1, 12>, foot::ratio>> {};
struct mile : derived_unit<mile, "mi", length, ratio_multiply<ratio<1760>, yard::ratio>> {};

With NTTP the implementation and usage of the ratio are much easier:

struct ratio {
  std::intmax_t num;
  std::intmax_t den;

  explicit constexpr ratio(std::intmax_t n, std::intmax_t d = 1) :
    num(n * (d < 0 ? -1 : 1) / std::gcd(n, d)),
    den(abs(d) / std::gcd(n, d))
  {
  }

  [[nodiscard]] constexpr bool operator==(const ratio&) = default;

  [[nodiscard]] friend constexpr ratio operator*(const ratio& lhs, const ratio& rhs)
  {
    const std::intmax_t gcd1 = std::gcd(lhs.num, rhs.den);
    const std::intmax_t gcd2 = std::gcd(rhs.num, lhs.den);
    return ratio(safe_multiply(lhs.num / gcd1, rhs.num / gcd2),
                 safe_multiply(lhs.den / gcd2, rhs.den / gcd1));
  }

  [[nodiscard]] friend constexpr ratio operator*(std::intmax_t n, const ratio& rhs)
  {
    return ratio(n) * rhs;
  }

  [[nodiscard]] friend constexpr ratio operator*(const ratio& lhs, std::intmax_t n)
  {
    return lhs * ratio(n);
  }
};
// US customary units
struct yard : derived_unit<yard, "yd", length, ratio(9144, 10000)> {};
struct foot : derived_unit<foot, "ft", length, yard::ratio / 3> {};
struct inch : derived_unit<inch, "in", length, foot::ratio / 12> {};
struct mile : derived_unit<mile, "mi", length, 1760 * yard::ratio> {};

Also, please see the mixed approach described in § 13.1 How to represent SI prefixes and derived units? which opens the door to new natural syntax of spelling units.

13.3. Relative vs absolute quantity

One of the most critical aspects of the physical units library is to understand what a quantity is? An absolute or relative value? For most dimensions only relative values have sense. For example:

However, for some base quantities like temperature, absolute values are really needed. For example, how much is 0+ 0? Is it 0 or 0 or 273.15? Yes, the repeated value of 0 is not an error here ;-) Actually, all of the answers are right:

As proven 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 only relative quantity values will be modeled by the library. Moreover, providing support for only relative quantities of other temperature units than Kelvin will probably still be misused by the users. This is why we suggest to support only Kelvins as built-in temperature units and provide verbose non-member utility functions for conversions between different kinds of temperature values and their units.

Alternatively, we could consider:

  1. Providing a fully featured engine to implement an affine space. For example quantity<absolute<units>>, Rep> (like in [BOOST.UNITS]) or quantity_point (like in [BENRI]) could be used for this purpose. If we decide to go this route, how to distinguish between UDLs for the same unit addressing such two abstractions?

  2. The solution based on [CPPNOW17-UNITS] where Kelvin is always treated as relative temperature and Celsius and Fahrenheit are always treated as an absolute one.

13.4. Should we support systems as a separate type?

For many years we have requirements to support multiple systems of units. Walter E. Brown started to present papers on this subject more than 20 years ago [WALTER_E_BROWN].

The basic rationale for a system of units is that "Unit systems should form closed universes independent from each other" as stated by [P1930R0]. But what does it really mean? Does it mean that every system should be independent from others and define its own dimensions and their units? Does it mean that it should not be possible to mix units from different systems even with explicit casts? If yes, are we ready to reimplement most of the same dimensional logic for every system from scratch?

US Customary System has the same dimensions and most of the units as the SI with the differences scoped mostly only in length and mass units and derived quantities using those. From the implementation and standardization point of view it is much simpler to use the common definitions of such physical dimensions and just provide units dedicated to such a system next to the SI ones (i.e. meters and miles). This is why even Boost.Units, the only library supporting systems, implements US units in SI system.

Another potential candidate for a dedicated system could be CGS (centimetre–gram–second) system, but even here all physical dimensions are defined in the same way so the units can, and in some cases should, be possible to be mixed with units from SI.

Even systems like coffee/milk/water/sugar system that seem to be totally isolated from typical SI use cases at some point will probably need time, volume, and other SI dimensions too.

This is why even when the library claims to support different unit systems it often does that based on SI anyway (i.e. [BENRI] provides headers like si/cgs.h).

Boost.Units uses systems mostly to provide the capability of having a different base unit for a dimension to limit intermediate conversions while passing quantities as vocabulary types in the interfaces. Usage of templates functions constrained with concepts for generic algorithms and concrete types for domain-specific needs addresses this area easily. For more information on this subject please refer to § 8 Limiting intermediate quantity value conversions.

Important point to note here is that adding direct systems support in the library type system might negatively affect user experience. Most of the verbose compilation errors presented in § 7.1 Type aliasing issues are caused by a dedicated systems support in Boost.Units.

Please also note that the author of the only library providing direct systems support [BOOST.UNITS] removed them in the refreshed design of a new library (§ 5.2.1 Design).

For example here is how we can support CGS without having direct systems support:

namespace cgs {

  using units::centimetre;
  using units::gram;
  using units::second;
  struct centimetre_per_second : units::deduced_derived_unit<centimetre_per_second, "cm/s", units::velocity, centimetre, second> {};
  struct gal : units::deduced_derived_unit<gal, "Gal", units::acceleration, centimetre, second> {};
  struct dyne : units::deduced_derived_unit<dyne, "dyn", units::force, centimetre, gram, second> {};
  struct erg : units::deduced_derived_unit<erg, "erg", units::energy, centimetre, gram, second> {};
  struct ergps : units::deduced_derived_unit<ergps, "erg/s", units::power, centimetre, gram, second> {};
  struct barye : units::deduced_derived_unit<barye, "Ba", units::pressure, centimetre, gram, second> {};


  inline namespace literals {

    using namespace units::literals;

    constexpr auto operator""cmps(unsigned long long l) { return units::quantity<centimetre_per_second, std::int64_t>(l); }
    constexpr auto operator""cmps(long double l) { return units::quantity<centimetre_per_second, long double>(l); }
    constexpr auto operator""Gal(unsigned long long l) { return units::quantity<gal, std::int64_t>(l); }
    constexpr auto operator""Gal(long double l) { return units::quantity<gal, long double>(l); }
    constexpr auto operator""dyn(unsigned long long l) { return units::quantity<dyne, std::int64_t>(l); }
    constexpr auto operator""dyn(long double l) { return units::quantity<dyne, long double>(l); }
    constexpr auto operator""_erg(unsigned long long l) { return units::quantity<erg, std::int64_t>(l); }
    constexpr auto operator""_erg(long double l) { return units::quantity<erg, long double>(l); }
    constexpr auto operator""_ergps(unsigned long long l) { return units::quantity<ergps, std::int64_t>(l); }
    constexpr auto operator""_ergps(long double l) { return units::quantity<ergps, long double>(l); }
    constexpr auto operator""Ba(unsigned long long l) { return units::quantity<barye, std::int64_t>(l); }
    constexpr auto operator""Ba(long double l) { return units::quantity<barye, long double>(l); }

  }  // namespace literals

}
using namespace cgs::literals;

static_assert(100cm == 1m);
static_assert(1000g == 1kg);
static_assert(100cmps == 1mps);
static_assert(100Gal == 1mps_sq);
static_assert(100000dyn == 1N);
static_assert(10000000_erg == 1_J);
static_assert(10000000_ergps == 1W);
static_assert(10Ba == 1Pa);

13.5. Interoperability with std::chrono::duration

One of the most challenging problems to solve in the physical units library will be the interoperability with std::chrono::duration. std::chrono 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:

  1. It addresses only one of many dimensions, namely time. There is no possibility to extend it with other dimensions support.

  2. quantity class template needs a few more member functions to provide support for conversions between different dimensions.

  3. SG6 members raised an issue with std::chrono::duration returning std::common_type_t<Rep1, Rep2> 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.

  4. std::ratio is not able to handle large prefixes required by some units (more information in § 9 std::ratio on steroids).

Because of the above issues we cannot just use std::chrono::duration design as it is right now and use it for physical units implementation or even as a representation of only time dimension. There are however, a few possibilities here to provide interoperability between the types:

  1. One of the solutions could be making a std::chrono::duration an alias or a child class of std::units::quantity class template (assuming that we will not use NTTP ratios as described in § 13.2 NTTP usage). This would be probably the best solution from the API point of view but unfortunately it will cause an ABI break.

  2. Provide built-in conversion facility that among others can be used to allow conversion between std::chrono::duration and std::units::quantity.

  3. Provide non-member function to convert and compare between those two types.

  4. Just ignore std::chrono::duration and do not provide any conversion utilities in the standard library.

From all options above we propose the first one if we decide that C++23 will be an ABI breaking release. In such a case we could update std::ratio type as described in § 9 std::ratio on steroids. Otherwise, we would probably go with the option #3.

13.6. Should we provide integral UDLs?

User Defined Literals support is really handy for the end-users. However, it sometimes might cause more confusion than benefits. For example defining both UDL versions for a velocity:

inline namespace literals {

  constexpr auto operator""kmph(unsigned long long l)
  { return quantity<kilometre_per_hour, std::int64_t>(l); }

  constexpr auto operator""kmph(long double l)
  { return quantity<kilometre_per_hour, long double>(l); }

}

and a function template defined as:

constexpr units::Velocity auto avg_speed(units::Length auto d, units::Time auto t)
{
  return d / t;
}

might cause the following code to not compile:

const units::Velocity auto speed = avg_speed(220km, 2h);

while the following one compiles fine:

const units::Velocity auto speed = avg_speed(220.km, 2h);

Above is caused by the constraints copied from std::chrono::duration and put on the conversion constructors requiring the denominator of ratio to be 1 in case of the integral representation type.

Based on the above we could agree on making integral UDLs returning a quantity with long double as a representation type. However, if we consider a base quantity like a digital information, what does it mean to have a fraction of bit? This probably is not the only one isolated example when actually only integral UDLs have sense.

Summarizing above we have the following options to choose from as an answer to "Should we provide integral UDLs?":

  1. Yes, as is (always both integral and floating-point for all units). And leave it up to the user to use them correctly.

  2. Yes, but integral literals get floating-point Rep.

  3. Yes, but only for specific units like a bit, byte, etc. where floating-point types do not have much sense (no floating-point UDLs in such case).

13.7. quantity<dim_length, metre> or quantity<metre>?

The initial version of the mp-units library provided the following quantity class template definition:

template<Dimension D, Unit U, Scalar Rep>
  requires std::same_as<D, typename U::dimension>
class quantity;

This allowed the following helper aliases:

template<Unit U = meter, Scalar Rep = double>
using length = quantity<dimension_length, U, Rep>;

With such a framework and CTAD usage user could write the following:

units::length d(3);                // 3 meters
units::length<units::mile> d3(3);  // 3 miles

or

units::velocity speed = avg_speed(220.km, 2.h);

or

template<typename U, typename Rep>
void foo(units::length<U, Rep> dist);

to constrain the type to a length dimension.

The downside of such a design was that the dimension was provided twice in every quantity class template instantiation which was affecting user experience by longer types in error logs or during debugging:

error: conversion fromquantity<units::dimension<units::exp<units::base_dim_length, 1>,
units::exp<units::base_dim_time, 1> >, units::unit<units::dimension<units::exp<
units::base_dim_length, 1>, units::exp<units::base_dim_time, 1> >, std::ratio<3600000, 1> >,
[...]>to non-scalar typequantity<units::dimension_velocity, units::kilometer_per_hour,
[...]>requested

During evening session in Cologne the author received a feedback from SG6 members that such a duplication should be removed. Right now the design looks as follows:

template<Unit U, Scalar Rep>
class quantity;

With this there is no possibility to provide a helper alias for a length dimension and above examples have to be implemented in terms of concepts:

units::quantity<units::metre> d(3);  // 3 meters
units::quantity<units::mile> d3(3);  // 3 miles

or

units::Velocity auto speed = avg_speed(220.km, 2.h);

or

template<units::Length Quantity>
void foo(Quantity dist);

The good part here is that the error logs are more readable with such an approach:

error: conversion fromquantity<units::unit<units::dimension<units::exp<units::base_dim_length,
1>, units::exp<units::base_dim_time, 1> >, std::ratio<3600000, 1> >, [...]>to non-scalar typequantity<units::kilometer_per_hour, [...]>requested

Both cases provide the similar functionality so it is a matter of taste here on which of the syntaxes the Committee will choose to continue with.

13.8. Should we provide seconds<int> or stay with quantity<second, int>?

Some of the users complain that writing quantity<second>(123) is too verbose and they would prefer a helper alias that would allow them to write seconds(123). This however, starts to generate a few issues:

We can consider renaming second to unit_second and provide seconds as an alias to the quantity class template. However, this will probably set in stone usage of aliases as no one will be willing to write a verbose code like quantity<unit_second>(123). This is why we are looking for a concrete guideline on which of the options the Committee prefers.

Author preference is to stay with the current design and leave it up to the users to create any helper aliases for their domains and use cases if they choose so (i.e. s(123)).

13.9. Should we provide support for dimensionless quantities?

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. For example: plane angle, solid angle, refractive index, relative permeability, mass fraction, friction factor, Mach number, etc. However, should all such division results be treated as dimensionless quantities rather than scalars?

auto q1 = 10s / 2s;
auto q2 = 90kmph / 30kmph;

If not, how to determine when to return a scalar an when the dimensionless quantity?

Numbers of entities are also quantities of dimension one. For example: number of turns in a coil, number of molecules in a given sample, degeneracy of the energy levels of a quantum system.

Should the library treat such entities as regular scalars or should some strong typing mechanism be provided to support those?

13.10. Number concept

Assuming that we will vote for a widespread concepts usage in the library it would be also nice to have a concept for scalars. Currently [MP-UNITS] defines a scalar as:

template<typename T>
concept Number = std::regular<T> &&
    std::totally_ordered<T> &&
    requires(T a, T b) {
      { a + b } -> std::same_as<T>;
      { a - b } -> std::same_as<T>;
      { a * b } -> std::same_as<T>;
      { a / b } -> std::same_as<T>;
      { +a } -> std::same_as<T>;
      { -a } -> std::same_as<T>;
      { a += b } -> std::same_as<T&>;
      { a -= b } -> std::same_as<T&>;
      { a *= b } -> std::same_as<T&>;
      { a /= b } -> std::same_as<T&>;
      { T{0} };
};

template<typename T>
concept Scalar = (!Quantity<T>) && Number<T>;

13.11. What about Unicode?

Every unit and its prefix can be printed with ASCII characters but it will probably not result with the best user experience. Quantities can often be represented better with Unicode symbols. For example an ASCII text output can look like:

10 us
2 kg*m/s^2
37 deg. C

while the same quantities can be represented as:

10 μs
2 kgm/s²
37 °C

If we decide to continue with the second approach it is not clear how to achieve this. For example std::chrono::duration is specified to return μs if the character U+00B5 can be represented in the encoding used for charT, us otherwise. However this is specified for:

template<class charT, class traits, class Rep, class Period>
basic_ostream<charT, traits>& operator<<(basic_ostream<charT, traits>& os,
                                         const duration<Rep, Period>& d);

which requires charT to be char or wchar_t. C++20 specification does not mention Unicode characters at all here. Even if we decide to provide a support for Unicode character sets (charN_t) here, it is not clear how to create such streams. Currently we do not have std::u8cout or even std::u8ostream in the C++ Standard Library.

Also, providing partial specializations to define such unit symbols:

template<typename CharT, typename Traits, typename Prefix, typename Ratio>
inline constexpr std::basic_string_view<CharT, Traits> prefix_symbol;

// one definition for all character types
template<typename CharT, typename Traits>
inline constexpr std::basic_string_view<CharT, Traits>
    prefix_symbol<CharT, Traits, si_prefix, std::milli> = "m";

// definition for non-Unicode systems
template<typename Traits>
inline constexpr std::basic_string_view<char, Traits>
    prefix_symbol<char, Traits, si_prefix, std::micro> = "u";
template<typename Traits>
inline constexpr std::basic_string_view<wchar_t, Traits>
    prefix_symbol<wchar_t, Traits, si_prefix, std::micro> = L"u";

// definition for Unicode
template<typename Traits>
inline constexpr std::basic_string_view<std::char8_t, Traits>
    prefix_symbol<std::char8_t, Traits, si_prefix, std::micro> = u8"µ";
template<typename Traits>
inline constexpr std::basic_string_view<std::char16_t, Traits>
    prefix_symbol<std::char16_t, Traits, si_prefix, std::micro> = u"µ";
template<typename Traits>
inline constexpr std::basic_string_view<std::char32_t, Traits>
    prefix_symbol<std::char32_t, Traits, si_prefix, std::micro> = U"µ";

starts to be a nightmare for library developers. Even, though we provided dedicated specializations for all character types for std::micro (which is actually a lot of boilerplate code), the first definition of std::milli will not compile for other character types than char. Right now there is no possibility in the C++ standard to define one literal that will allow to initialize all versions of such a variable template. If we had such a feature, additionally, it would be great to be able to initialize all non-Unicode character types with one literal and all Unicode character types with another one using Unicode character set. If we had such facility we could implement std::micro case from above with only two partial specializations:

// definition for non-Unicode systems
template<non_unicode charT, typename Traits>
inline constexpr std::basic_string_view<charT, Traits>
    prefix_symbol<charT, Traits, si_prefix, std::micro> = NON_UNICODE("u");

// definition for Unicode
template<unicode charT, typename Traits>
inline constexpr std::basic_string_view<charT, Traits>
    prefix_symbol<charT, Traits, si_prefix, std::micro> = UNICODE("µ");

14. Impact on the Standard

The library would be mostly a pure addition to the C++ Standard Library with the following potential exceptions:

  1. It is unclear how to provide interoperability with the std::chrono::duration (more information in § 13.5 Interoperability with std::chrono::duration).

  2. std::units::ratio will most probably need to be a different type with the similar semantics to std::ratio (more information in § 9 std::ratio on steroids). However, if we decide C++23 to be an ABI breaking release we could update std::ratio with an additional template parameter.

15. Implementation Experience

The author of this document implemented mp-units [MP-UNITS] library, where he tested different ideas and proved the implementability of the features described in the paper.

15.1. Usage example

#include <units/dimensions/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::literals;

  const auto v = avg_speed(220.km, 2h);
  assert(v.count() == 110);   // passes
  assert(v == 110kmph);       // passes
  std::cout << v << '\n';     // prints "110 km/h"
}

void test2()
{
  using namespace units::literals;

  const auto v = avg_speed(units::quantity<units::mile>(140), units::quantity<units::hour>(2));
  assert(v.count() == 70);    // passes
  assert(v == 70mph);         // passes
  std::cout << v << '\n';     // prints "70 mi/h"
}

Compiler Explorer

15.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::quantity<units::kilometre, double> d1(123);
auto d2 = 123km;    // units::quantity<units::kilometre, std::int64_t>

There are C++ concepts provided for each such quantity type:

template<typename T>
concept Length = QuantityOf<T, 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:

const units::quantity<units::kilometre> d(220);
const units::quantity<units::hour> t(2);
const units::Velocity auto kmph = units::quantity_cast<units::kilometre_per_hour>(avg_speed(d, t));
std::cout << kmph.count() << " km/h\n";

const units::Velocity auto speed = avg_speed(140.mi, 2.h);
assert(speed.count() == 70);
std::cout << units::quantity_cast<units::mile_per_hour>(speed).count() << " mph\n";

This guarantees that no intermediate conversions are being made, and the output binary is as effective as implementing the function with doubles.

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::unit<units::dimension<units::exp<units::base_dim_length, 1, 1>,
                units::exp<units::base_dim_time, 1, -1> >, units::ratio<5, 18> >, double>]

Yet, thanks to downcast facility, the actual error message is:

[with D = units::quantity<units::kilometre_per_hour, double>]

The breakpoint in the debugger became readable as well:

Breakpoint 1, avg_speed<units::quantity<units::kilometre, double>,
                        units::quantity<units::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. For example, if the user wants to provide a custom digital information base dimension and new units based on it, only minimal code is required:

#include <units/quantity.h>

using namespace units;

// custom base dimension
struct base_dim_digital_information : base_dimension<"digital information", "b"> {};

// custom derived dimension and its concept
struct digital_information : derived_dimension<digital_information,
                                               exp<base_dim_digital_information, 1>> {};

template<typename T>
concept DigitalInformation = QuantityOf<T, digital_information>;

// custom prefixes
struct data_prefix;
struct kibi : prefix<kibi, data_prefix, ratio<    1024>, "Ki"> {};
struct mebi : prefix<mebi, data_prefix, ratio<1048576>, "Mi"> {};

// custom units and their literals
struct bit : coherent_derived_unit<bit, "b", digital_information, data_prefix> {};
struct kilobit : prefixed_derived_unit<kilobit, kibi, bit> {};
struct byte : derived_unit<byte, "B", digital_information, ratio<8>> {};
struct kilobyte : prefixed_derived_unit<kilobyte, kibi, byte> {};

inline namespace literals {
  constexpr auto operator""_b(unsigned long long l) { return quantity<bit, std::int64_t>(l); }
  constexpr auto operator""_Kib(unsigned long long l) { return quantity<kilobit, std::int64_t>(l); }
  constexpr auto operator""_B(unsigned long long l) { return quantity<byte, std::int64_t>(l); }
  constexpr auto operator""_KiB(unsigned long long l) { return quantity<kilobyte, std::int64_t>(l); }
}

// unit tests
static_assert(1_B == 8_b);
static_assert(1024_b == 1_Kib);
static_assert(1024_B == 1_KiB);
static_assert(8 * 1024_b == 1_KiB);
static_assert(8 * 1_Kib == 1_KiB);

16. Polls

16.1. LEWG

  1. Do we want a physical units library in the C++ standard?

  2. Do we prefer UDL, multiply, or mixed syntax for units (§ 13.1 How to represent SI prefixes and derived units?)?

  3. Do we like the concept-based approach to prevent truncation and intermediate conversions (§ 8.2 Generic programming with concepts)?

  4. Do we like a downcasting facility and would like to standardize it as a part of std (§ 7.2 Downcasting facility)?

  5. Do we prefer NTTP usage for ratio and exp (§ 13.2 NTTP usage)?

  6. Which option of UDLs do we prefer (§ 13.6 Should we provide integral UDLs?)?

  7. Should American spelling be provided? (meter vs. metre, ton vs. tonne, ...)?

  8. Should we provide seconds<int> or stay with quantity<second, int> (§ 13.8 Should we provide seconds<int> or stay with quantity<second, int>?)?

  9. What about std::chrono::duration (§ 13.5 Interoperability with std::chrono::duration)?

16.2. SG6

  1. Do we want to have built-in support for digital information dimensions and its prefixes in the initial version of the library?

  2. Should we provide built-in support for some off-system units or limit to SI units only?

  3. Do we want to introduce a dedicated system type (§ 13.4 Should we support systems as a separate type?)?

  4. Do we want to require explicit representation casts between the same units of the same dimension, or do we allow chrono-like implicit conversions (floating-point representation and non-truncating integer conversions)?

  5. Do we want to require explicit unit casts between different units of the same dimension, or do we allow chrono-like implicit conversions (implicitly convert kilometre to metre)?

  6. Do we agree with Kelvins only support for temperature and verbose conversion functions for other units and absolute temperatures? Should affine types be provided (§ 13.3 Relative vs absolute quantity)?

  7. Do we want to provide support for runtime-specified conversions too (e.g. to support currency)?

  8. Should we provide support for dimensionless quantities or just use scalars (§ 13.9 Should we provide support for dimensionless quantities?)?

  9. Do we want to standardize a Number concept (§ 13.10 Number concept)?

  10. Should constants be provided? If yes, how should updates to the constants be handled? (Separate namespaces?)

  11. Which SI standards should be supported (ISO 80000-1:2009, SI 2019) be provided? How should updates to the ISO standard be handled? (Separate namespaces?)

16.3. SG16

  1. Should we use strings containing characters outside the basic source character set?

  2. Do we want a support for charN_t at all given the paltry support currently in the standard?

  3. Should we provide different symbol presentation for charN_t vs char and wchar_t?

  4. Should we provide localized unit names and symbols?

  5. Should we provide std::basic_fixed_string?

17. 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 mp-units library and this document.

Index

Terms defined by this specification

References

Normative References

[ISO_80000-1]
Quantities and units - Part 1: General. URL: https://www.iso.org/standard/30669.html

Informative References

[BENRI]
Jan A. Sende. benri. URL: https://github.com/jansende/benri
[BOOST.UNITS]
Steven Watanabe; Matthias C. Schabel. Boost.Units. URL: https://www.boost.org/doc/libs/1_70_0/doc/html/boost_units.html
[BRYAN_UNITS]
Bryan St. Amour. units. URL: https://github.com/bstamour/units
[CLARENCE]
Steve Chawkins. Mismeasure for Measure. URL: https://www.latimes.com/archives/la-xpm-2001-feb-09-me-23253-story.html
[COLUMBUS]
Christopher Columbus. URL: https://en.wikipedia.org/wiki/Christopher_Columbus
[CPPNOW17-UNITS]
Steven Watanabe. cppnow17-units. URL: https://github.com/swatanabe/cppnow17-units
[DISNEY]
Cause of the Space Mountain Incident Determined at Tokyo Disneyland Park. URL: https://web.archive.org/web/20040209033827/http://www.olc.co.jp/news/20040121_01en.html
[DUCHARME_UNITS]
Vincent Ducharme. units. URL: https://github.com/VincentDucharme/Units
[FLIGHT_6316]
Korean Air Flight 6316 MD-11, Shanghai, China - April 15, 1999. URL: https://ntsb.gov/news/press-releases/Pages/Korean_Air_Flight_6316_MD-11_Shanghai_China_-_April_15_1999.aspx
[GIMLI_GLIDER]
Gimli Glider. URL: https://en.wikipedia.org/wiki/Gimli_Glider
[MARS_ORBITER]
Mars Climate Orbiter. URL: https://en.wikipedia.org/wiki/Mars_Climate_Orbiter
[MIKEFORD3_UNITS]
Michael Ford. units. URL: https://github.com/mikeford3/units
[MP-UNITS]
Mateusz Pusz. mp-units. URL: https://github.com/mpusz/units
[NIC_UNITS]
Nic Holthaus. units. URL: https://github.com/nholthaus/units
[P1930R0]
Vincent Reverdy. Towards a standard unit systems library. 7 October 2019. URL: https://wg21.link/p1930r0
[PHYSUNITS-CT-CPP11]
Martin Moene. PhysUnits-CT-Cpp11. URL: https://github.com/martinmoene/PhysUnits-CT-Cpp11
[WALTER_E_BROWN]
Walter E. Brown. Introduction to the SI Library of Unit-Based Computation. URL: https://lss.fnal.gov/archive/1998/conf/Conf-98-328.pdf
[WILD_RICE]
Manufacturers, exporters think metric. URL: https://www.bizjournals.com/eastbay/stories/2001/07/09/focus3.html