Let the Compiler Check Your Units

Let the Compiler Check Your Units

By Wu Yongwei

Overload, 34(192):10-15, April 2026


Mixing your units can be disastrous. Wu Yongwei takes a quick look at C++ unit libraries that can help keep everything in order.

I recently came across a C++ standard proposal P3045 [P3045R7], which aims to add physical units to C++. Curious, I looked into the existing unit libraries and went down quite a rabbit hole.

Type safety and user-defined literals

Before exploring these libraries, I was already somewhat familiar with the idea of ‘type safety’. I was also aware that user-defined literals (UDLs) [CppReference-1] allow creating literals of specific types with ease. Typical uses in the standard library include string/string_view literals and the chrono library [CppReference-2], which make code both convenient and safe.

Figure 1 shows some simple examples.

auto msg = "Hello "s + user_name;
auto t1 = chrono::steady_clock::now();
this_thread::sleep_for(500ms);
auto t2 = chrono::steady_clock::now();
auto duration = t2 - t1;
auto what = t1 + t2;      // Can't compile
cout << duration / 1.0ms; // To double, in ms
Figure 1

Unlike a plain time_t, where addition, subtraction, multiplication, and division all compile regardless of whether they make sense, the chrono library distinguishes between time points and durations. Only operations that are actually meaningful will compile, such as:

  • time point ± duration → time point
  • time point - time point → duration
  • scalar * duration → duration
  • duration * (or / or %) scalar → duration
  • duration / duration → scalar
  • duration % duration → duration

The duration types defined by the chrono library also encode the underlying data type (which arithmetic type to use) and the ratio relative to the base unit. Implicit conversions must not truncate, e.g.:

  • A duration with an integer underlying type can be implicitly converted to one with a floating-point underlying type, but not vice versa.
  • When both underlying types are integral, a coarser-precision duration can be implicitly converted to a finer-precision one (e.g. from seconds to milliseconds), but not vice versa.

We can write our own UDLs, and the only difference from the standard library is that UDL suffixes must begin with an underscore ( _). Listing 1 is a simple example.

class length {
public:
  explicit length(double v) : value_(v) {}
  double value() const { return value_; };
private:
  double value_;
};
length operator+(length lhs, length rhs)
{
  return length(lhs.value() + rhs.value());
}
length operator""_m(long double v)
{
  return length{static_cast<double>(v)};
}
length operator""_cm(long double v)
{
  return length{static_cast<double>(v) * 0.01};
}
Listing 1

With this, we can write ‘1.0_m + 10.0_cm’ and get length{1.1}. Pretty simple, right?

When I cover UDLs in training sessions, I usually mention the story of the Mars Climate Orbiter: software from Lockheed Martin reportedly produced thrust data in pound-force seconds, while NASA’s navigation system expected newton-seconds. The unit mismatch caused the Orbiter to enter an incorrect orbit and ultimately disintegrate (you can search for more details). This incident is often cited to illustrate the risks of unit mix-ups. If UDLs had existed back then, and NASA had been using C++ with such features, could the accident have been avoided? Of course, 1998 feels like ancient times in the tech world: software tools were not as powerful as they are today, and the first C++ standard had only just appeared.

Back to the code above, we can already see a downside of this approach: when a floating-point number is followed by a literal suffix, the compiler always passes the number as a long double to the corresponding UDL operator. Here I unconditionally cast to double, but this introduces a potential truncation issue. I will return to this point later.

Unit libraries using UDLs

Unit libraries built on strong types existed even before C++11, and Boost.Units [Boost.Units] is one such example. However, it does not use UDLs and has seen little subsequent development, so I won’t cover it here. The code examples in Listing 2 compile with two unit libraries that are still maintained (though not under active development): PhysUnits-CT-Cpp11 [PhysUnits-CT-Cpp11] and SI [SI].

auto mass = 2.0_kg + 500_g;
auto distance = 9.8_m;
auto acceleration = distance / 1_s / 1_s;
auto force = mass * acceleration;
std::cout << "Mass:         " << mass << '\n';
std::cout << "Distance:     " << distance
          << '\n';
std::cout << "Acceleration: " << acceleration
          << '\n';
std::cout << "Force:        " << force << '\n';
Listing 2

The first two lines of output are probably what you’d expect, but the ‘Force’ line might come as a surprise. With the PhysUnits-CT-Cpp11 library, the output is:

  Mass:         2.5 kg
  Distance:     9.8 m
  Acceleration: 9.8 m s-2
  Force:        24.5 N

With the SI library, the output is (same result, slightly different formatting):

  Mass:         2.5 kg
  Distance:     9.8 m
  Acceleration: 9.8 m/s^2
  Force:        24.5 N

Both libraries support unit composition, and both know that in the International System of Units (abbreviated SI), kg · m/s2 is just N (newton), and display the result in newtons directly.

The two libraries are very similar in functionality, both using dimensions to constrain the allowed operations (see more details about ‘dimensional analysis’ in [Wikipedia]). Quantities of the same dimension can be added or subtracted, while multiplication and division can produce new dimensions. From the SI perspective, the dimension of mass is M (with unit kg), the dimension of acceleration is LT-2 (length divided by the square of time, with unit m/s2), and multiplying the two yields LMT-2 (length times mass divided by the square of time, with unit kg · m/s2, or N).

From the user’s perspective, the main differences are:

  • PhysUnits-CT-Cpp11 uniformly uses long double as the underlying type, while SI automatically uses int64_t or long double depending on whether you write an integer or floating-point literal (2_kg vs. 2.0_kg). Combined with SI’s loose implicit conversion rules (the result type of an arithmetic operation is determined by the first argument), this has an unfortunate consequence: writing ‘2.0_kg + 500_g’ gives you 2.5_kg (underlying type long double), but writing ‘2_kg + 500_g’ gives you 2_kg (underlying type int64_t, with the result truncated to kilograms)!
  • PhysUnits-CT-Cpp11 allows arbitrary combinations of units, while SI requires you to include the corresponding header for each valid combination. For instance, if I hadn’t included <SI/acceleration.h>, the ‘auto acceleration = …’ line would have failed to compile.
  • PhysUnits-CT-Cpp11 requires at least the C++11 standard, while SI requires at least C++17.

If you only need basic SI unit support (and don’t need pounds, inches, etc.), and using long double is not a concern, I think PhysUnits-CT-Cpp11 is a solid choice – it is simple to use and provides basic unit type safety.

Unit libraries without UDLs

Problems with UDLs

You may have already noticed some issues with UDLs, but they haven’t been fully illustrated in the code above. The primary problem with UDLs is that the UDL parameter-passing mechanism does not simply forward the literal’s original type – it forces conversion through one of a few fixed forms. For numeric literals with UDL suffixes (e.g. 10_m), there are three ways to define a UDL:

  • One approach uses the form ‘operator""_m(unsigned long long)’ (for integers) or ‘operator""_m(long double)’ (for floating-point numbers). If we want to use a narrower type to represent values (such as int or double), truncation or precision loss can occur (though this is not generally a problem, and C++20’s consteval can allow compile-time checks).
  • Another approach uses the form ‘operator""_m(const char*)’, where the compiler passes the number (e.g. 10) in as a string. This forces us to decide in advance which concrete arithmetic type to use for the result; if it’s not large enough, truncation or precision loss can again occur.
  • A third approach uses template parameters (‘template <char...> … operator""_m()’), which is the most flexible – it can produce different result types based on the specific input – but also the most complex to implement.

The consequences of this are:

  • UDLs cannot directly represent negative numbers. To represent a negative value, you need to define a unary operator- on the result type.
  • The way UDLs are defined makes it difficult to flexibly choose the underlying arithmetic type. The standard library handles complex numbers by using different suffixes: 1i is a complex<double>, 1if is a complex<float>, and so on (the underlying types are always floating-point). This is reasonably natural and consistent for complex numbers, but clearly doesn’t work for units like the metre (m) or the kilogram (kg) …
  • The value of a quantity can be a complex number, which would be impractical to combine with another UDL suffix representing the unit.
  • For compound units (such as the acceleration above, with unit m/s2), using UDLs means either defining a large number of suffixes (SI does define _m_p_s and _km_p_h, but does not include a UDL suffix for acceleration), or composing with unit literals (such as 1_s) as in the example code above.

For these reasons, some newer unit libraries have abandoned UDLs in favour of arithmetic composition with unit constants. (In fact, in PhysUnits-CT-Cpp11 you can write ‘9.8_m / second / second’ or even ‘9.8 * meter / second / second’.)

The Au library

In its own words, Au [Au] is:

A C++14-compatible physical units library with no dependencies and a single-file delivery option. Emphasis on safety, accessibility, performance, and developer experience.

The earlier code needs some adjustments to work with Au. Since Au is a library I’d genuinely recommend, I’ll present the full code in Listing 3.

#include <au/io.hh>
#include <au/prefix.hh>
#include <au/units/grams.hh>
#include <au/units/meters.hh>
#include <au/units/newtons.hh>
#include <au/units/seconds.hh>
#include <iostream>
using namespace au;
using namespace au::symbols;
constexpr auto kg = symbol_for(kilo(grams));
int main()
{
  // Create quantities
  auto mass = 2.0 * kg + 500 * g;
  auto distance = 9.8 * m;
  auto acceleration = distance / s / s;
  // Calculation
  auto force = mass * acceleration;
  // Output
  std::cout << "Mass:              " << mass
            << '\n';
  std::cout << "Mass (as kg):      "
            << mass.as(kg) << '\n';
  std::cout << "Distance:          " << distance
            << '\n';
  std::cout << "Acceleration:      "
            << acceleration << '\n';
  std::cout << "Force (compound):  " << force
            << '\n';
  std::cout << "Force (as Newton): "
            << force.as(N) << '\n';
Listing 3

The output is:

  Mass:              2500 g
  Mass (as kg):      2.5 kg
  Distance:          9.8 m
  Acceleration:      9.8 m / s^2
  Force (compound):  24500 (m * g) / s^2
  Force (as Newton): 24.5 N

Some differences are immediately apparent:

  • Units are no longer UDL suffixes but constants. Writing ‘2.0 * kg’ may seem slightly more verbose than ‘2.0_kg’, but creating the acceleration (‘distance / s / s’, or ‘9.8 * (m/s/s)’) is actually more concise. Also, defining new units becomes very easy (Au itself only defines base units like g and s, plus prefixes like kilo and milli, without defining kg or ms), and the underlying type of a physical quantity becomes intuitive: it’s simply the type of the literal itself. In the expressions above, the underlying type is double rather than long double (in the mass case it is the result of adding a double and an int).
  • As before, we can conveniently compose new physical quantities through arithmetic, but since Au is not SI-centric, results are not automatically converted to SI units. However, we can use as to perform conversions, provided that the dimensions match and Au does not consider the conversion risky (more on this later); otherwise, the code will not compile. For example, we can write ‘(2.0 * kg + 500 * g).as(kg)’, but not ‘(2 * kg + 500 * g).as(kg)’. If we need to allow potentially risky conversions, we can use ‘(2 * kg + 500 * g).coerce_as(kg)’ – the result is 2 * kg, i.e. 2 kilograms (with int as the underlying type).

Note that the SI base unit for mass is the kilogram, not the gram. However, Au chooses the gram as the base unit at the programming level and composes the kilogram via prefixes, which unifies how prefixes are used.

Au defines not only common base units and prefixes, but also other commonly used units such as the inch used in English-speaking countries (by including <au/units/inches.hh>). It encodes in the type system that 1 inch equals 254/100 centimetres, and defines:

  • The print label as "in"
  • The singular and plural quantity maker constants as inch and inches
  • The symbol as in

Defining custom units is straightforward too. The code below defines a modern Chinese ‘chǐ’ (尺). It uses the C++17 syntax (C++14-style code would be slightly more complicated):

  struct Chi
    : decltype(au::Meters{} / au::mag<3>()) {
    static constexpr const char label[] = "chǐ";
  };
  constexpr auto chi = au::QuantityMaker<Chi>{};

Its meaning is straightforward:

  • 1 metre (the code uses the American spelling Meters) equals 3 chǐ.
  • The output label is ‘chǐ’.
  • The constant representing the unit in code is chi, so writing ‘cout << distance.as(chi)’ gives us the output ‘29.4 chǐ’.

We’ve already discussed as, and in is another important member function for an Au quantity. quantity.as(unit) returns a new Quantity object (still a quantity with a unit), while quantity.in(unit) returns the underlying numerical value (a scalar without units). For example, distance.in(m) returns the double value 9.8. This comes in handy when interfacing with APIs that don’t use the unit library. Similarly, coerce_as has a corresponding coerce_in.

Au has a few more features worth highlighting:

  • All of Au’s default unit checks happen at compile time. The generated machine code is essentially identical to hand-written scalar arithmetic – necessary multiplication/division conversions are still there, but there is no extra runtime overhead. This is C++’s zero-overhead abstraction principle in action.
  • Au uses exact rational arithmetic for integer conversions (e.g. inches to cm is stored as 254/100). To prevent data loss, the as member function forbids conversions that result in truncation or high overflow risk. For example, you cannot strictly convert inches to centimetres (truncation) or int32_t inches to nanometres (high overflow risk) without an explicit risk-ignoring parameter (or the coerce_as member function).

    Warning: Because Au performs the multiplication before the division (e.g. grams to pounds is to multiply by 1,000,000 and then divide by 453,592,370), forced integer conversions can easily overflow intermediate values and cause undefined behaviour! (Floating-point arithmetic has no such problems.)

  • Au has QuantityPoint for ‘absolute values’ and plain Quantity for ‘relative quantities’, mirroring the design of chrono’s time_point and duration. For example, a thermometer reading of 20 °C is a QuantityPoint (celsius_pt(20)), while a temperature rise of 5 °C is a Quantity (celsius_qty(5)). The type system enforces physical logic: you can add a Quantity to a QuantityPoint, but adding two absolute QuantityPoints together is a compile-time error.
  • Au also supports modern formatting – you can use it with C++20’s std::format and C++23’s std::print, not just traditional IO streams. It also works with the {fmt} [fmt] library.
  • If your project only needs a handful of units, Au provides a Python tool that can bundle just the units you need into a single header file for direct inclusion in your project.

Au has also put significant efforts into error message readability. When you write code with mismatched dimensions, the compiler error messages will contain reasonably readable unit descriptions, making it easier to track down the problem. In contrast, some earlier unit libraries (such as Boost.Units) were notorious for their extremely verbose and hard-to-read error messages.

The mp-units library

mp-units [mp-units] is currently the most feature-complete and most modern C++ unit library, created and led by Mateusz Pusz, the first author of P3045. It requires C++20 or later and makes heavy use of new features such as concepts and class-type non-type template parameters (NTTPs). It also serves as the foundation for the committee’s ongoing work on adding unit support to the standard library.

Code using mp-units is quite similar to Au (see Listing 4).

#include <iostream>
#include <mp-units/systems/si.h>
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
int main()
{
  // Create quantities
  auto mass = 2.0 * kg + 500 * g;
  auto distance = 9.8 * m;
  auto acceleration = distance / s / s;
  // Calculation
  auto force = mass * acceleration;
  // Output
  std::cout << "Mass:              " << mass
            << '\n';
  std::cout << "Mass (as kg):      "
            << mass.in(kg) << '\n';
  std::cout << "Distance:          " << distance
            << '\n';
  std::cout << "Acceleration:      "
            << acceleration << '\n';
  std::cout << "Force (compound):  " << force
            << '\n';
  std::cout << "Force (as Newton): "
            << force.in(N) << '\n';
}
Listing 4

Output:

  Mass:              2500 g
  Mass (as kg):      2.5 kg
  Distance:          9.8 m
  Acceleration:      9.8 m/s²
  Force (compound):  24500 g m/s²
  Force (as Newton): 24.5 N

Noticeably more modern and polished.

Now let’s look at the differences in the code. First, note some naming differences from Au: as in Au becomes in in mp-units; coerce_as becomes force_in; and in becomes numerical_value_in. In particular, both libraries have in, but with different semantics – you must be careful if you migrate code between the two libraries.

Another minor difference is that standard prefixed units are fully predefined in mp-units, such as kg and ms. Defining new units is also different, but overall simpler:

  // Since accented (or Chinese) characters are
  // not in the basic character set, we need to
  // use symbol_text
  inline constexpr struct chi final
    : named_unit<symbol_text{u8"chǐ", "chi"},
                 mag_ratio<1, 3> * si::metre> {
  } chi;

If we use only basic characters (a subset of ASCII) [CppReference-3], the code can be even simpler:

  // For symbols in the basic character set, we
  // don't need to use symbol_text
  inline constexpr struct chi final
    : named_unit<"chi",
                 mag_ratio<1, 3> * si::metre> {
  } chi;

This clearly expresses that 1 chǐ equals 1/3 metre. As with Au, we can now write ‘distance.in(chi)’.

Broadly speaking, mp-units is richer in features and more future-oriented:

  • mp-units introduces the concept of ‘quantity kind’, based on the International System of Quantities (ISQ). It distinguishes not only dimensions but also semantics, catching at compile time the mixing of quantities that share the same dimension but have different semantics.
  • mp-units requires C++20: it makes comprehensive use of concepts to constrain templates (there will be concept constraint examples below), and takes advantage of class-type NTTPs. Au defines a celsius_pt to help create temperature points, but mp-units users can simply write ‘point<deg_C>’ (deg_C is a constexpr class-type object, used as NTTP) to achieve the same purpose, simpler and more consistent.
  • mp-units is the basis for C++ standard proposals. If you want to align with the future standard as early as possible, mp-units’ API and conceptual framework will be closer to the final standardized form.

Let me elaborate on ‘quantity kind’. A simple example: width, height, and radius are all dimensionally lengths, but in mp-units they can be distinct quantity kinds, preventing conceptually meaningless mixing. Another example: energy and moment of force1 share the same dimension (both are L2MT-2, and both quantities can be written in units of kg · m2/s2), but passing an energy to a function expecting a moment of force could lead to disastrous consequences. mp-units will prevent the code in Listing 5 from compiling.

// Explicitly require the parameter to be moment
// of force / torque
void foo(
  QuantityOf<isq::moment_of_force> auto moment)
{
  // …
}
// Explicitly mark work as energy
auto work = isq::energy(100 * J);
// work does NOT satisfy foo's constraint
foo(work);
Listing 5

Whether you need this level of distinction depends on your requirements, but the option is there if you want it.

By the way, SI considers angles to be dimensionless – but this has always been contentious. You generally can’t mix angles, solid angles, and plain dimensionless numbers freely. Both Au and mp-units sidestep this, and provide automatic conversion between degrees and radians. In mp-units, for instance, the function in Listing 6 works regardless of whether you pass in degrees (e.g. 30 * deg) or radians (e.g. 0.5 * π * rad – yes, this works).

void print_angle(
  QuantityOf<isq::angular_measure> auto angle)
{
  auto a = value_cast<double>(angle);
  std::cout << "Angle:   " << a.in(deg) << '\n';
  std::cout << "Radians: " << a.in(rad) << '\n';
  std::cout << "sin:     " << sin(a) << '\n';
}
Listing 6

Here, value_cast ensures the underlying type is double; otherwise, when an integer quantity is passed in, one of the subsequent in conversions will fail to compile.

If your project is already using C++20 or later, and you need comprehensive unit support or quantity kind distinction, mp-units should be a good choice. If your project needs to be compatible with C++14/17 or only requires basic SI unit safety, Au is more lightweight and still useful (the example code of Au compiles noticeably faster than that of mp-units). The core design philosophies of both (compile-time checking, zero runtime overhead, composition by multiplication or division) are the same.

Finally, let me demonstrate that unit checking typically incurs no runtime overhead. At https://godbolt.org/z/aKbsG4P5Y, you can see that mass * acceleration is compiled into the following single assembly instruction (line 2):

  mulsd   xmm0, xmm1

Very clean!

A standard unit library for C++29

The C++ standards committee is actively working towards incorporating unit and physical quantity support into the standard library. Several related proposals cover this space, including P1935 [P1935R2] (the initial proposal), P2980 [P2980R1] (motivation, scope, and plan), P2981 [P2981R1] (safety discussion), P2982 [P2982R1] (discussion of quantity as a numeric type), and P3045 (mentioned at the beginning of this article). The core content of these proposals is based on mp-units’ design and implementation experience.

This feature may make it into the C++29 standard (it definitely won’t make C++26). The design space is large, and many issues still need discussion and resolution. The current P3045R6 proposal is already very long – it’s a hefty read on its own.

An important design consideration for the standard unit library is integration with the existing chrono library. Chrono’s duration and time_point are essentially physical quantities and quantity points in the time dimension. Ideally, the new unit library should be able to subsume or generalize chrono’s design. This is a recurring theme across the proposals.

Once a standard unit library lands, it will provide the C++ ecosystem with a unified representation for physical quantities. Scientific computing, engineering software, embedded systems, game engines, and other domains will all be able to interoperate under the same type system, instead of each reinventing the wheel. On a practical note, even though C++29 is still several years away, learning about and using existing libraries like Au or mp-units is already worthwhile – their core concepts and API designs closely track where the standard is headed, and migration costs should be low.2

I hope this gives you a clearer picture of what’s available. Zero-overhead abstraction remains one of C++’s greatest strengths, and unit libraries are a perfect showcase for it.

References

[Au] https://github.com/aurora-opensource/au

[Boost.Units] https://www.boost.org/doc/libs/latest/doc/html/boost_units.html

[CppReference-1] cppreference.com, ‘User-defined literals’, https://en.cppreference.com/w/cpp/language/user_literal.html

[CppReference-2] cppreference.com, ‘Date and time library’, https://en.cppreference.com/w/cpp/chrono.html

[CppReference-3] cppreference.com, ‘Character sets and encodings’, https://en.cppreference.com/w/cpp/language/charset.html

[fmt] https://github.com/fmtlib/fmt

[mp-units] https://github.com/mpusz/mp-units

[P1935R2] Mateusz Pusz, ‘A C++ approach to physical units’, https://wg21.link/p1935r2

[P2980R1] Mateusz Pusz et al., ‘A motivation, scope, and plan for a quantities and units library’, https://wg21.link/p2980r1

[P2981R1] Mateusz Pusz et al., ‘Improving our safety with a physical quantities and units library’, https://wg21.link/p2981r1

[P2982R1] Mateusz Pusz and Chip Hogg, ‘std::quantity as a numeric type’, https://wg21.link/p2982r1

[P3045R7] Mateusz Pusz et al., ‘Quantities and units library’, https://wg21.link/p3045r7

[PhysUnits-CT-Cpp11] https://github.com/martinmoene/PhysUnits-CT-Cpp11

[SI] https://github.com/bernedom/SI

[Wikipedia] Wikipedia, ‘Dimensional analysis’, https://en.wikipedia.org/wiki/Dimensional_analysis

Footnotes

  1. 1 Commonly called torque, though ISQ and mp-units treat torque as a narrower, scalar form of moment of force.
  2. 2 It is no coincidence that P3045’s authors include the lead developers of mp-units, Au, and SI.

Wu Yongwei Having been a programmer and software architect, Yongwei is currently a consultant and trainer on contemporary C++. He has 30 years’ experience in systems programming and architecture in C and C++. His focus is on the C++ language, software architecture, performance tuning, design patterns, and code reuse. He has a programming page at http://wyw.dcweb.cn/






Your Privacy

By clicking "Accept Non-Essential Cookies" you agree ACCU can store non-essential cookies on your device and disclose information in accordance with our Privacy Policy and Cookie Policy.

Current Setting: Non-Essential Cookies REJECTED


By clicking "Include Third Party Content" you agree ACCU can forward your IP address to third-party sites (such as YouTube) to enhance the information presented on this site, and that third-party sites may store cookies on your device.

Current Setting: Third Party Content EXCLUDED



Settings can be changed at any time from the Cookie Policy page.