Type-Safe Units in C++: What the Compiler Can Catch Before the Spacecraft Crashes
Source: isocpp
In 1999, NASA lost a $327 million spacecraft because one software module produced thruster values in pound-force while the receiving module expected newtons. The difference is a factor of roughly 4.45, and it was enough to send the Mars Climate Orbiter into the wrong orbital entry angle and burn it up in the Martian atmosphere. The navigation team noticed the anomaly 14 months before the loss and filed reports that went unaddressed. The compiler, had it been given the right types to reason about, would not have ignored the report.
This is the motivating example behind P3045R7, a proposal currently targeting C++26 that would bring physical units into the standard library. Wu Yongwei’s recent tour of the existing unit library landscape is a good entry point into this space, but the rabbit hole goes considerably deeper than a survey allows. The design decisions required to make a units library correct, ergonomic, and zero-cost reveal some genuinely interesting tensions in the C++ type system.
What std::chrono Already Proved
The standard library has been doing compile-time dimensional analysis since C++11, quietly, through std::chrono. The duration types are parameterized on a ratio representing the tick period, and arithmetic between durations of different periods is fully checked at compile time:
auto t1 = chrono::steady_clock::now();
this_thread::sleep_for(500ms);
auto t2 = chrono::steady_clock::now();
auto duration = t2 - t1; // duration_t, safe
auto what = t1 + t2; // Won't compile: can't add two time_points
cout << duration / 1.0ms; // Explicit conversion to double milliseconds
The line t1 + t2 is a type error because adding two absolute timestamps is semantically meaningless, just as adding a temperature in Kelvin to another temperature in Kelvin gives you a number without physical meaning. The chrono library encodes this distinction by separating duration (a difference) from time_point (an absolute position on a timeline). This is the affine space model, and it turns out to be one of the trickiest parts of generalizing units to the full physical domain.
Three Generations of C++ Unit Libraries
The existing library ecosystem reflects the evolution of the language itself.
Boost.Units came first and is the most battle-tested. It predates C++11 and was built with heavy template metaprogramming, representing dimensions as compile-time lists of exponents over a set of base dimensions. The result is correct but verbose. Error messages involving boost::units::unit<boost::units::derived_dimension<...>> are famously hostile. It works, but the ergonomics reflect a language era where this kind of thing required a lot of machinery.
nholthaus/units was written after the Mars Climate Orbiter disaster prompted its author to ask: what if the compiler had caught that? It targets C++14, ships as a single header with no dependencies, and compiles in under a second for most use cases. The design is simpler than Boost.Units and considerably more approachable:
using namespace units::literals;
using namespace units::force;
using namespace units::mass;
using namespace units::acceleration;
newton_t thrust = 100.0_N;
pounds_force_t thrust_lbf = thrust; // Explicit conversion, no surprise
auto bad = thrust + 1.0_m; // Compile error: force + length
The library uses user-defined literals extensively and provides a large vocabulary of predefined units. It is header-only, integrates with std::chrono, and is probably the easiest entry point for projects that want unit safety without committing to a more ambitious dependency. The tradeoff is that it is less expressive around quantities versus quantity points, and it targets C++14 rather than C++20, so it does not use concepts for its constraints.
mp-units, developed by Mateusz Pusz, is the modern answer and serves as the reference implementation for P3045. It requires C++20 and uses concepts throughout, which produces dramatically better error messages when you violate a constraint. The design distinguishes between quantity types (differences, deltas) and quantity_point types (absolute positions), inheriting and generalizing the pattern chrono established:
// A quantity is a delta: how much of something
quantity<si::metre> displacement = 5 * m;
quantity<si::kelvin> temp_delta = 10 * K;
// A quantity_point is an absolute location
quantity_point<si::celsius, ice_point> room_temp = 20 * deg_C;
quantity_point<si::kelvin, absolute_zero> boiling = 373.15 * K;
// You can subtract two quantity_points to get a quantity
auto delta = boiling - room_temp; // quantity<si::kelvin>
// You cannot add two quantity_points
auto invalid = room_temp + boiling; // Compile error
The ice_point and absolute_zero origins encode the fact that Celsius and Kelvin have different zero references. This matters enormously in practice. You can add 10 degrees to a temperature (shifting it along the scale), but adding two temperatures together is meaningless. The library catches both errors at compile time.
The Affine Space Problem
The quantity/quantity_point distinction is where most simpler implementations fall short. Consider geographic position: latitude and longitude are quantity_points with no natural zero that has physical meaning for most navigation purposes. The difference between two positions is a displacement vector, a quantity. Adding two positions is undefined. GPS coordinates, timestamps, and temperatures all share this structure, and getting it wrong produces subtle bugs that are harder to catch than a pure type mismatch.
Boost.Units does not cleanly model this distinction; it represents everything as a quantity scaled from some unit. nholthaus/units has similar limitations. mp-units and the P3045 proposal treat the affine space model as a first-class concern, following the design precedent set by chrono.
There is an additional subtlety around conversion safety. When converting between units of the same dimension, implicit narrowing is dangerous: converting meters to kilometers without care can silently truncate integer values. mp-units follows chrono’s approach of allowing implicit conversions only when they are lossless (e.g., km to m with integer representation) and requiring explicit quantity_cast when truncation is possible.
Comparison with Rust’s Approach
Rust’s uom crate solves the same problem and is worth examining for perspective. It uses Rust’s trait system and const generics to represent dimensions, achieving zero-cost abstractions through monomorphization. The ergonomics differ mainly in syntax:
use uom::si::f64::*;
use uom::si::length::meter;
use uom::si::time::second;
let distance = Length::new::<meter>(5.0);
let time = Time::new::<second>(2.0);
let speed = distance / time; // Velocity, checked at compile time
Rust’s type system makes it somewhat easier to express the zero-cost constraint without worrying about implicit conversions, since Rust has fewer implicit coercions in general. The C++ situation is complicated by the language’s broad set of numeric conversion rules, which is part of why mp-units is conservative about implicit conversions.
Both ecosystems converge on the same insight: the type system is expressive enough to encode physical dimensionality, and doing so eliminates an entire class of runtime bugs at zero runtime cost.
Where P3045 Stands and What It Means for C++26
P3045R7 is at revision seven as of mid-2025, targeting C++26. The proposal is substantially based on mp-units and addresses dimensions, units, quantities, quantity points, and text output for units. The scope is broad: it aims to cover the full SI system and enough of the US customary system to handle practical engineering needs.
The proposal is still under review in the Library Evolution Working Group (LEWG), and inclusion in C++26 is not guaranteed. The design is largely settled, but the scope of the standard library addition is large, and standardization timelines are notoriously slippery.
In the meantime, mp-units is available today via vcpkg with vcpkg install mp-units, and its API closely tracks what the standard proposal will look like. Projects that adopt it now will have a short migration path once P3045 lands.
Practical Considerations
For a new project in C++20 or later, mp-units is the strongest choice if physical correctness matters. The C++20 concepts requirement is not a burden for projects written in the last few years, and the error messages are substantially better than older template-metaprogramming approaches. The vcpkg availability removes the most common objection to library dependencies.
For projects constrained to C++14 or that need a minimal footprint, nholthaus/units is a reasonable pick. It is well-maintained, widely tested, and covers most common use cases in physics and engineering. The quantity_point gap only matters if your domain requires distinguishing absolute positions from differences.
For legacy codebases with Boost already in the dependency tree, Boost.Units provides correctness at the cost of ergonomics. The verbosity is real, but it beats untyped doubles.
The case against using any of these libraries usually comes down to team familiarity and compilation overhead. The compilation overhead concern is real for Boost.Units in particular, but nholthaus/units is explicitly designed around compile time, and mp-units with C++20 modules (when toolchain support catches up) will reduce that overhead further.
The more interesting question is why this is still a library concern at all. The Fortran community had dimensional analysis proposals in the 1990s. Ada has built-in support for numeric types with range and representation constraints that approach this problem from a different angle. C++ has had the expressive power to encode this in the type system for over a decade, and the chrono library has been demonstrating the pattern in the standard since C++11. P3045 is the long overdue acknowledgment that what chrono does for time should be available for the full physical domain.
The compiler has always been able to catch the Mars Climate Orbiter bug. We just had to give it the right types to reason about.