In 1999, NASA’s Mars Climate Orbiter disintegrated in the Martian atmosphere because one piece of software produced thruster data in pound-force seconds while another expected newton-seconds. The spacecraft cost $327 million. The fix would have been a type error.
That story has been retold so many times it has become a cliché, but it keeps coming up because the underlying problem is still live. Most C++ code that deals with physical quantities passes raw double values around, trusting comments and variable names to carry unit information. The compiler sees no difference between meters and feet, or between velocity and acceleration. Wu Yongwei’s recent article on isocpp.org surveys the library landscape that has grown up around this problem, motivated by P3045R7, the proposal to add a quantities and units library to the C++ standard. The survey is a good entry point. What it does not do is dig into why the API design of these libraries looks the way it does, or what the progression from Boost.Units to mp-units actually represents as a progression of C++ technique.
Where std::chrono Already Solved This
The standard library already contains a working unit system: std::chrono. It is narrow in scope, covering only time, but its design illustrates everything that makes compile-time unit checking possible.
auto t1 = std::chrono::steady_clock::now();
std::this_thread::sleep_for(500ms);
auto t2 = std::chrono::steady_clock::now();
auto duration = t2 - t1; // compiles fine
auto bad = t1 + t2; // does not compile
std::cout << duration / 1.0ms; // elapsed time as a double in ms
The key mechanism is that time_point and duration are distinct types, and arithmetic between them is selectively defined. You can subtract two time_point values to get a duration, but adding them makes no physical sense and the compiler refuses. The unit itself, milliseconds or seconds or hours, is encoded in the type via std::chrono::duration<Rep, Period>, so implicit narrowing conversions are rejected unless you explicitly call duration_cast.
This is the template the general-purpose unit libraries try to extend to every physical quantity, not just time.
Boost.Units: Correct but Verbose
Boost.Units arrived in Boost 1.36.0 in 2008 and was for a long time the only serious option for compile-time dimensional analysis in C++. It works with C++03 through heavy template metaprogramming, encoding dimensions as mpl::list types.
#include <boost/units/systems/si.hpp>
#include <boost/units/io.hpp>
using namespace boost::units;
using namespace boost::units::si;
quantity<length> d = 100.0 * meters;
quantity<time_dimension> t = 9.58 * seconds;
quantity<velocity> v = d / t; // ~10.44 m/s, type-checked
// Does not compile:
// quantity<velocity> bad = d + t;
The dimensional analysis is correct. The ergonomics are rough. Error messages are notoriously long because they expose the underlying mpl::list representation. Writing user-defined literals requires significant boilerplate. Boost.Units proved the concept was viable in production, and it remains in use in embedded and scientific codebases that need C++03 compatibility, but most developers found the learning curve steep relative to the protection offered.
nholthaus/units: The Pragmatic Middle Ground
nholthaus/units took a different approach when it appeared around 2016. It targets C++14, is header-only with no dependencies, and prioritizes usability over theoretical completeness.
#include <units.h>
using namespace units::literals;
using namespace units::velocity;
auto dist = 100.0_m;
auto t = 9.58_s;
auto spd = dist / t; // unit_t<meters_per_second>, type-checked
// Explicit conversion:
auto spd_kph = units::convert<kilometers_per_hour>(spd);
// Does not compile:
// auto bad = dist + t;
The library defines hundreds of unit types, handles non-linear conversions like Celsius to Fahrenheit, and produces readable error messages by C++14 standards. The user-defined literal syntax makes measurement quantities feel natural.
The limitation is that nholthaus/units treats all length quantities as interchangeable. A function taking unit_t<meter> will accept both a displacement and a position vector; the library cannot distinguish between them because both have the same dimension. For many use cases that is fine. For scientific or aerospace code, the distinction matters.
mp-units: Where C++20 Changes the Game
mp-units is the library that Mateusz Pusz has been developing as the reference implementation for P3045. It requires C++20 and uses concepts, non-type template parameters, and std::format integration to achieve an API that is both terse and expressive.
#include <mp-units/systems/si.h>
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
auto d = 85.0 * km;
auto t = 1.5 * h;
auto v = d / t; // quantity<isq::speed[km/h], double>
std::println("{}", v); // "56.6667 km/h"
std::println("{}", v.in(m/s)); // "15.7407 m/s"
The km/h syntax works because km and h are inline variable templates, and the division operator produces a derived unit type. No string parsing, no runtime lookup. Everything resolves at compile time.
The std::format integration is first-class:
std::println("{:%Q %q}", 60.0 * km / h); // "60 km/h"
std::println("{::N[.2f]}", 60.0 * km / h); // "60.00 km/h"
The ISQ Distinction: Named Quantities
The most conceptually interesting part of mp-units, and what distinguishes it most sharply from its predecessors, is its support for the International System of Quantities (ISQ), not just SI units.
The ISQ defines named quantity types: length, width, height, thickness, distance, radius. They all share the dimension L, but they are semantically distinct. A function that takes the radius of a circle should not silently accept a value representing the height of a box, even though both are measured in meters.
mp-units encodes this:
using namespace mp_units::isq;
quantity<width[m]> w = 1.0 * m;
quantity<height[m]> h = 2.0 * m;
// w and h are different types; mixing requires explicit acknowledgment
auto area = w * h; // quantity<width * height[m^2]>
This is a significant step beyond what Boost.Units offers. Boost.Units tracks dimension (length, time, mass) but not quantity kind. mp-units tracks both, which means it can catch bugs that pure dimensional analysis misses: passing the wrong length-typed value to the wrong parameter.
The affine space support for temperatures shows the same level of precision:
quantity_point<si::celsius[deg_C]> t1 = 20.0 * deg_C;
quantity_point<si::celsius[deg_C]> t2 = 25.0 * deg_C;
auto diff = t2 - t1; // quantity<isq::temperature_difference[deg_C]>
auto sum = t1 + t2; // does not compile
Subtracting two temperatures gives a temperature difference; adding them makes no physical sense. The type system enforces this without any runtime cost.
How Rust Handles the Same Problem
The uom crate is the dominant unit library in Rust. It uses const generics and trait bounds to encode dimensional exponents directly in type parameters:
use uom::si::f64::*;
use uom::si::length::meter;
use uom::si::time::second;
use uom::si::velocity::meter_per_second;
let d = Length::new::<meter>(100.0);
let t = Time::new::<second>(9.58);
let v: Velocity = d / t; // enforced by trait bounds
println!("{} m/s", v.get::<meter_per_second>());
Rust’s trait system makes this pattern feel native to the language. Compiler errors when dimensions mismatch are cleaner because the type representations are simpler than C++ template instantiation chains. uom does not yet implement the ISQ named-quantity distinction, placing it closer to nholthaus/units in that regard, but the ergonomics of library-versus-language fit are noticeably better than any C++ solution before mp-units.
C++20 concepts partially close this gap. mp-units uses concepts to constrain template parameters and produce cleaner error messages than Boost.Units ever could. The gap is narrower now.
P3045R7 and the Road to Standardization
P3045 has been evolving for several years. It missed the C++26 feature freeze and is currently targeting C++29. The proposal is extensive: SI base and derived units, non-SI units accepted for use with SI (liter, tonne, electronvolt), prefixes, the full ISQ quantity hierarchy, affine spaces for temperatures, and std::format integration.
The scope is part of why it has not moved faster. Adding a units library to the standard is not like adding std::span. It requires committee consensus on which quantity names to include, how to handle customization for domain-specific units, and how to resolve ambiguous cases in the ISQ itself. The ISQ has some quantities with overlapping definitions that require explicit design decisions.
In the meantime, using mp-units in new code is a reasonable choice. It is MIT-licensed, actively maintained, and the API will be close to whatever eventually lands in the standard, since P3045 is built directly from mp-units experience. For projects where C++20 is not available, nholthaus/units is the most accessible option. For projects already on Boost, Boost.Units has the advantage of already being present.
The Argument Against Waiting
The argument against adopting any of these libraries is that the overhead, in compile time and learning curve, is not worth it for codebases that have managed fine with raw doubles. That argument has force for small projects.
For anything doing real physics or engineering calculations, the counterargument is that the bugs these libraries prevent tend to be silent. A Mars Climate Orbiter bug does not crash at startup; it runs fine for months and then produces incorrect results when the units interact in unexpected ways. Unit errors are exactly the category of bug that code review, testing, and static analysis tend to miss, because the values look plausible at every step. The compiler, given enough type information, can catch them for free at build time.
The standard library already made this argument for time with std::chrono. P3045 is the same argument extended to every physical quantity. The fact that the ecosystem has been building toward this for over fifteen years, from Boost.Units through nholthaus/units to mp-units, suggests both that the problem is real and that the solution is maturing. The compiler has always been willing to help here; the question is whether to give it enough information to do so.