In 1999, NASA lost a $328 million spacecraft because one software component produced thruster values in pound-force seconds while another expected newton-seconds. The Mars Climate Orbiter burned up in the Martian atmosphere. The unit mismatch was invisible to the type system, invisible to the compiler, and invisible right up until the mission ended.
This story gets cited so often it risks becoming a cliche, but the underlying problem remains unsolved in most codebases. A double is a double. The compiler cannot know whether it represents meters, miles, or microfurlongs unless you build that information into the type. Wu Yongwei’s recent overview on isocpp.org surveys the libraries that address this and points toward P3045, the proposal to bring physical units into the C++ standard. The article is a good map of the territory. What it does not fully explore is how differently each library approaches the problem, and why the most recent entrant represents a qualitatively different kind of solution.
The Foundation: <chrono> Gets It Right
The standard library already demonstrates the correct pattern. You can write 500ms and get a std::chrono::duration<long long, std::milli>. You cannot add two time_point values together; the operation simply will not compile. The distinction between a point in time and an interval between two points is encoded in the type.
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: duration
auto what = t1 + t2; // Compile error: can't add two time_points
std::cout << duration / 1.0ms; // numeric ratio, in milliseconds
This is dimensional analysis expressed through C++ types, and it costs nothing at runtime because the type information is erased at codegen. User-defined literals, available since C++11, make the syntax ergonomic. 500ms, 2s, 1h construct the appropriate specialization with no overhead.
The broader question is whether the same approach scales to a general physical unit system covering mass, length, velocity, pressure, electrical quantities, and their combinations.
nholthaus/units: The C++14 Workhorse
Before mp-units, the most widely adopted answer was nholthaus/units, a single-header C++14 library. Its design is practical: include one header, use unit literals, get compile-time checking.
#include <units.h>
using namespace units::literals;
auto distance = 60.0_km;
auto time = 2.0_h;
auto velocity = distance / time; // kilometers_per_hour_t
units::length::meter_t in_meters(distance); // explicit conversion: 60000 m
units::mass::kilogram_t mass(70.0);
// auto bad = distance + mass; // compile error
The library works by giving each unit a distinct type. kilometer_t and kilogram_t are unrelated types; adding them fails. Unit conversions require explicit construction of the target type, which prevents silent precision loss.
The limitations are design-level rather than implementation bugs. nholthaus/units encodes dimensions but not quantities in the richer sense. In the International System of Quantities (ISQ), width and height are both lengths, yet they are conceptually distinct and should not be interchangeable in interfaces that care about the difference. nholthaus/units cannot represent this; both are just meter_t. Similarly, angular velocity (radians per second) and frequency (hertz, also per second) are dimensionally identical but physically different, and the library cannot distinguish them.
Compiler error messages from nholthaus/units also tend toward the verbose and opaque, which was a persistent complaint from teams adopting it.
mp-units: The C++23 Redesign
mp-units by Mateusz Pusz targets C++23 and treats the ISQ as a first-class citizen rather than an afterthought. The difference in expressive power is significant.
#include <mp-units/systems/si.h>
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
auto d = 60 * km;
auto t = 2 * h;
auto v = d / t; // quantity<isq::speed[km/h]>
// Explicit conversion required
auto v_mps = v.in(m/s); // 16.666... m/s
// This does not compile:
auto wrong = d + t;
The more important distinction emerges with angular velocity vs. frequency:
// Both reduce to s^-1 dimensionally, yet they are physically different
auto omega = 60 * rad/s; // angular velocity
auto freq = 60 * Hz; // frequency
// mp-units represents this distinction via the ISQ quantity hierarchy
// nholthaus/units cannot
mp-units also introduces quantity points, a concept from affine spaces. A quantity is a difference: a displacement, a duration, a temperature change. A quantity point is an absolute position in a measurement space. The difference between two quantity points is a quantity; adding two quantity points is undefined. This is exactly what <chrono> does for time, generalized across all dimensions.
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
quantity_point<isq::thermodynamic_temperature[deg_C]> boiling{100};
quantity_point<isq::thermodynamic_temperature[deg_C]> freezing{0};
auto diff = boiling - freezing; // 100 deg_C (a quantity)
// auto sum = boiling + freezing; // compile error
This distinction matters for temperature unit conversions. Converting 25 degC to Fahrenheit as a reading gives 77 degF. Converting a 25 degC temperature difference gives 45 degF. These are different operations. Libraries that conflate quantity points with quantities will silently produce wrong answers for one case or the other.
What P3045 Adds
mp-units is the reference implementation for P3045, the proposal to add a quantities and units library to the C++ standard. R7 is the latest revision as of 2025, under active review by SG6 (Numerics) and LEWG.
The proposal encodes the full ISQ hierarchy, not just SI dimensions. This means the standard library would distinguish isq::width from isq::height from isq::distance even though all three are lengths in SI terms. Interfaces can accept only a width, making it impossible to accidentally pass a height or a raw meter value.
Zero overhead is a hard requirement: quantities and quantity points must compile down to the same machine code as the underlying numeric types when optimizations are enabled. The type information exists only at compile time. There is no runtime tag, no virtual dispatch, no heap allocation.
For C++26, the proposal is likely too late. C++29 is the more realistic target. In the meantime, mp-units is available today and tracks the proposal closely enough that code written against it should require minimal changes to migrate to a standard version.
The Boost.Units Predecessor
For historical completeness: Boost.Units pioneered compile-time dimensional analysis in C++ around 2008. Its design is more complex to use, it requires the full Boost dependency, and the template error messages it produces are nearly unreadable. It remains functional but is rarely chosen for new projects when lighter alternatives exist.
Choosing a Library Today
For projects targeting C++14 or C++17 with no access to a C++23 compiler, nholthaus/units is the practical choice. It covers the common cases, the API is straightforward, and a single header is easy to vendor.
For projects on C++23 or newer, mp-units is the better investment. The ISQ quantity hierarchy catches a class of bugs that dimensional analysis alone cannot. The error messages are substantially more useful, since C++20 concepts give the compiler precise constraint information to report at the point of failure rather than deep in template instantiation. Staying close to P3045 also means your code will age well as the standard evolves.
The migration path from untyped double values is incremental. Introduce unit types at API boundaries first, let the compiler surface the implicit conversions, and fix them one by one. You do not need to convert an entire codebase at once.
The Broader Point
Unit errors are not exotic edge cases confined to aerospace software. They show up in game physics when mixing meters and units-per-second in velocity calculations. They show up in financial code when basis points get treated as percentages. They appear constantly in embedded sensor code when raw ADC counts get mixed with calibrated physical values without a clear conversion boundary.
C++ has had the tools to solve this problem since C++11 via user-defined literals and constexpr, and the <chrono> library has demonstrated for fifteen years that the pattern works well in practice. mp-units extends it to the full physical world. The question is not really whether to use a units library but how long to wait before the cost of not using one becomes obvious in the bug tracker.