C++ Is Finally Getting a Units Library, and the Design Journey Is Worth Understanding
Source: isocpp
In September 1999, NASA lost a 327-million-dollar spacecraft because one software team produced thruster output in pound-force seconds and another expected newton seconds. The Mars Climate Orbiter disintegrated entering the Martian atmosphere, and the root cause was a unit mismatch that no type system caught because nobody asked the type system to.
That failure is now a canonical teaching example, but the engineering lesson is still being absorbed. C++ has had the tools to prevent this class of bug for over a decade. Wu Yongwei’s recent walkthrough on isocpp.org is a good entry point into the landscape, but the story goes deeper than a library survey. What we are really watching is C++ slowly growing into the idea that physical quantities deserve the same first-class treatment as any other type.
The Proof of Concept Was Already in the Standard Library
std::chrono has been in the standard since C++11, and it demonstrates the model clearly. A duration is not a bare number; it carries its unit as a compile-time ratio. You cannot accidentally add a milliseconds value to a seconds value and get garbage, because the library defines the semantics of mixing them. You cannot add two time points together, because that makes no physical sense:
auto t1 = std::chrono::steady_clock::now();
std::this_thread::sleep_for(500ms);
auto t2 = std::chrono::steady_clock::now();
auto duration = t2 - t1; // ok: time_point - time_point = duration
auto nonsense = t1 + t2; // compile error: can't add two time_points
cout << duration / 1.0ms; // prints elapsed milliseconds as double
This is not magic. It is a consequence of the library encoding physical meaning into the type system. time_point subtraction is defined; time_point addition is not, because physically there is no such operation. Every serious unit library applies the same idea to a broader domain.
The Template Mechanics
The core technique is straightforward. You represent a unit as a compile-time encoding of its dimension exponents. For SI base units, you need seven exponents: length, mass, time, electric current, temperature, luminous intensity, and amount of substance. A velocity has exponents (1, 0, -1, 0, 0, 0, 0), meaning one power of length and negative one power of time.
A simplified version illustrates the principle:
template <int Meters, int Kilograms, int Seconds>
struct Unit {};
template <typename U>
struct Quantity {
explicit Quantity(double value) : value_(value) {}
double value_;
};
// Addition only compiles when units match exactly
template <typename U>
Quantity<U> operator+(Quantity<U> a, Quantity<U> b) {
return Quantity<U>(a.value_ + b.value_);
}
// Multiplication combines exponents at compile time
template <int M1, int K1, int S1, int M2, int K2, int S2>
Quantity<Unit<M1+M2, K1+K2, S1+S2>>
operator*(Quantity<Unit<M1,K1,S1>> a, Quantity<Unit<M2,K2,S2>> b) {
return Quantity<Unit<M1+M2, K1+K2, S1+S2>>(a.value_ * b.value_);
}
With this in place, multiplying a Quantity<Unit<1,0,0>> (meters) by a Quantity<Unit<0,1,0>> (kilograms) produces a Quantity<Unit<1,1,0>>, not a compile error and not a bare double. Division works the same way, subtracting exponents. Attempting to add meters to seconds fails to compile because the template argument lists differ. The compiler does the physics checking; you get the error at build time rather than during a mission-critical maneuver.
Three Libraries, Three Eras
Boost.Units predates C++11 and remains the most mature option for legacy codebases. It uses Boost.MPL to compute dimension arithmetic at compile time, which means it carries Boost as a dependency and imposes notoriously long compile times. The API is verbose by modern standards:
boost::units::quantity<boost::units::si::length> d(
5.0 * boost::units::si::meter);
boost::units::quantity<boost::units::si::time> t(
2.0 * boost::units::si::second);
auto v = d / t; // quantity<velocity>
It works correctly. It just feels like it was written before auto existed, which it was.
nholthaus/units arrived for C++14 and trades breadth for approachability. It is header-only with no external dependencies, compiles significantly faster than Boost.Units, and provides user-defined literals for common cases:
#include <units.h>
using namespace units::literals;
auto length = 5.0_m;
auto time = 2.0_s;
auto speed = length / time; // unit_t<meters_per_second>
For embedded and constrained environments this is often the right pick. The library covers most SI units and many non-SI ones. Its main limitation is that error messages when you misuse it are not always readable, and extending it with custom dimensions takes more effort than what C++20 features make possible.
mp-units is the current state of the art, built for C++20 and using concepts and consteval throughout. It is the basis for P3045R7, the proposal targeting standardization. The API reads cleanly:
#include <mp-units/systems/si.h>
using namespace mp_units::si::unit_symbols;
auto length = 5.0 * m;
auto time = 2.0 * s;
auto speed = length / time; // quantity<m/s, double>
// Explicit conversion when needed
auto in_km_per_h = speed.in(km / h);
The use of C++20 concepts means that error messages when you mix incompatible units are substantially more readable than what Boost.Units or nholthaus produces. The compiler can explain what dimension was expected and what was provided, rather than unrolling a page of template instantiations.
Also worth mentioning is Au from Aurora Innovation, the self-driving vehicle company. It was extracted from their internal production codebase and open-sourced at C++Now 2023. Autonomous vehicles compute constant streams of velocities, accelerations, distances, and time intervals, and getting a unit wrong in that context has real consequences. Au’s design reflects that pressure. One of its deliberate choices is that you cannot extract a raw number from a quantity without explicitly naming the unit you want:
#include <au/au.hh>
auto distance = au::meters(100.0);
auto time = au::seconds(9.58);
auto speed = distance / time;
// You must name the unit to get the number out:
double mps = speed.in(au::meters / au::seconds); // ~10.43
This forces the unit to be visible at every point where the abstraction boundary is crossed, which is exactly the kind of enforcement that matters in safety-critical code.
The Quantity vs. Quantity Point Distinction
One of the more subtle design decisions in P3045R7 concerns the difference between a quantity and a quantity point, and it matters more than it might appear.
A quantity is a relative measurement: 10 kilometers, 5 seconds, 3 degrees of temperature change. A quantity point is an absolute position in a measurement space: the year 2024, a GPS coordinate, 37 degrees Celsius as a body temperature reading. The operations valid on each differ.
Consider temperature. Adding 30°C and 20°C makes no physical sense if both are absolute temperatures, but subtracting them gives a valid 10 K temperature difference. The affine space rules that P3045R7 encodes are:
- Quantity + Quantity = Quantity
- QuantityPoint - QuantityPoint = Quantity
- QuantityPoint + Quantity = QuantityPoint
- QuantityPoint - Quantity = QuantityPoint
std::chrono already follows this model. time_point - time_point yields a duration; time_point + time_point does not compile. P3045R7 generalizes this to every dimension. Without this distinction, a library can correctly prevent you from adding meters to seconds but still allow physically nonsensical addition of absolute temperature readings.
What Standardization Actually Changes
Libraries like mp-units work today. So why does it matter whether physical units land in the standard?
Two reasons. First, adoption. Libraries that require pulling in a dependency get skipped in environments with strict dependency policies, which includes much of embedded development, safety-critical systems, and large enterprise codebases. Something in <units> gets used in the same situations where std::chrono gets used: universally and without debate.
Second, interoperability. Right now, a function that returns a nholthaus::unit_t<meters> cannot directly accept a value computed using mp-units without a conversion layer. Standard types remove this friction at the boundary between libraries and components. This is the same reason std::string and std::vector matter even though equivalent third-party containers existed before them.
P3045R7 is currently targeting C++26 or C++29 depending on committee bandwidth. The proposal is substantive: it covers SI base units and derived units, conversion factors, quantity points, user-defined literals, and physical constants.
The Compile Time Trade-off
The honest cost of unit libraries is compile time. Template-heavy dimension arithmetic is work the compiler has to do, and it adds up in large translation units. Boost.Units is the most expensive; nholthaus/units is considerably faster; mp-units sits between the two, with C++20 module support helping at the upper end of codebase sizes.
For most projects this is not a blocking concern. Unit checking is typically applied at architectural boundaries and in domain model code, not uniformly throughout an entire codebase. The places where unit bugs actually happen, interfaces between subsystems with different conventions, are also the places where paying a few hundred extra milliseconds of compile time per translation unit is a reasonable trade.
The runtime overhead is zero. All of the dimension arithmetic happens at compile time. The generated code is identical to what you would get from bare double operations. You get full type safety and no performance penalty, only a build-time cost.
Writing Code That Cannot Have This Class of Bug
The machinery has existed for years. std::chrono proved the model. Boost.Units showed it could generalize. nholthaus made it accessible. mp-units made it ergonomic. P3045R7 is making the argument that it belongs in every C++ compiler’s standard library.
The Mars Climate Orbiter burned up 25 years ago. If the navigation software had been written with a unit-aware type system, the compiler would have rejected the interface mismatch before the code shipped. The types would have been wrong, not just the values, and the error would have appeared on a developer’s screen rather than 140 million kilometers away.
That is what this entire thread of work is building toward: a world where that category of mistake is a compile error, not a nine-digit lesson about the importance of consistent units.