· 6 min read ·

Type-Safe Units in C++: What the Compiler Can Catch Before Your Spacecraft Burns Up

Source: isocpp

In 1999, NASA lost a $328 million spacecraft because one engineering team delivered thruster data in pound-force seconds while another team’s software expected newton-seconds. The Mars Climate Orbiter burned up in the Martian atmosphere over a unit mismatch that no type system caught, because the values were plain floating-point numbers the whole way down.

That incident gets cited constantly in discussions about type-safe units, and for good reason. The fix is obvious in retrospect: if the thruster values had carried their unit in their type, the compiler would have rejected the assignment or forced an explicit conversion. The bug becomes a build error. This is the core promise of dimensional analysis libraries for C++, and it is a promise the language has been chasing for decades.

What the Standard Already Does

The standard library demonstrates the approach works with <chrono>. Duration types like std::chrono::milliseconds and std::chrono::seconds encode their unit in the type, so this compiles:

auto t1 = std::chrono::steady_clock::now();
std::this_thread::sleep_for(500ms);
auto t2 = std::chrono::steady_clock::now();
auto duration = t2 - t1;
std::cout << duration / 1.0ms;  // prints elapsed time in ms

But this does not:

auto nonsense = t1 + t2;  // error: no operator+ for two time_points

Subtracting two time_point values yields a duration. Adding two time_point values is physically meaningless and the compiler refuses it. Converting between durations with different periods is automatic when lossless and requires an explicit cast when it truncates.

This is exactly what a units library should do for every physical dimension, not just time. The problem is the language community spent twenty years not agreeing on how to do it.

Three Generations of Libraries

Boost.Units is the oldest serious attempt, predating C++11. Its design is thorough but shows its age. The API is verbose, template instantiation depths are significant, and compile times suffer on large codebases. It works, and real scientific projects still depend on it, but you would not choose it for a greenfield project today.

#include <boost/units/systems/si.hpp>
using namespace boost::units;
using namespace boost::units::si;

quantity<length> l1 = 1.0 * meter;
quantity<length> l2 = 2.0 * meter;
quantity<area> a = l1 * l2;  // type-safe: area, not length
quantity<volume> wrong = l1 * l2;  // compile error

The types work. The ergonomics require patience.

nholthaus/units was a C++14 refresh that cleaned up the API and delivered a header-only approach with much more approachable syntax. It became popular for embedded and scientific projects where Boost was too heavy a dependency. User-defined literals make call sites readable:

#include <units.h>
using namespace units::literals;

auto distance = 100.0_m;
auto time     = 9.58_s;
auto speed    = distance / time;  // deduced as meters_per_second_t<double>

It covers SI, CGS, imperial, and several other systems. The library is still maintained and represents a reasonable choice for C++14 or C++17 codebases.

mp-units is the current state of the art, built explicitly for C++20 and C++23. It is the reference implementation behind P3045R7, the ISO proposal to add quantities and units to the C++ standard library. The API makes full use of concepts, constexpr, and improved class template argument deduction, which makes both the call sites and the error messages substantially better than older designs.

What mp-units Actually Looks Like

The core type is quantity<Reference, Rep>, where Reference encodes both dimension and unit, and Rep is the underlying numeric type:

#include <mp-units/systems/si.h>
using namespace mp_units;
using namespace mp_units::si::unit_symbols;

quantity<si::metre, int> d = 100 * m;
quantity<si::second, double> t = 9.58 * s;
auto v = d / t;  // quantity<si::metre / si::second, double>

// This does not compile:
auto wrong = d + t;  // error: cannot add metres and seconds

Unit conversions that are lossless happen implicitly. Conversions that could truncate require an explicit call, mirroring the <chrono> design:

// Implicit: metres to millimetres is always exact for integers
quantity<si::milli<si::metre>, int> mm = 1 * m;  // fine

// Explicit: millimetres to metres would truncate
quantity<si::metre, int> m2 = (1500 * mm).in(m);  // requires .in()

One design detail that matters in practice is how the library handles affine quantities. Temperature is the standard example. Twenty-five degrees Celsius plus twenty-five degrees Celsius is not fifty degrees Celsius in any physically meaningful sense, because Celsius is an offset scale. You can add a temperature difference to a temperature, but you cannot add two absolute temperatures. mp-units models this distinction rigorously through quantity_point for absolute positions and quantity for displacements. Getting this wrong in an HVAC controller or chemistry simulation produces incorrect answers rather than crashes, which makes it harder to catch in testing.

The compile times are also meaningfully better than Boost.Units. The library uses concepts for constraints rather than SFINAE, which produces diagnostics that name the actual mistake rather than printing twelve nested template instantiations before pointing at the offending line.

The P3045 Standardization Effort

P3045 has been through seven revisions and was submitted to WG21 in late 2025. The scope is substantial: dimension systems, quantity types, affine types, named units across SI and several other systems, and user-defined literals. Each revision has addressed committee feedback, narrowing scope in some areas and hardening the design in others.

The proposal draws on everything that went wrong with earlier libraries. Where Boost.Units required separate unit system headers and different namespace hierarchies for CGS versus SI, P3045 builds a unified dimension hierarchy. Where nholthaus/units provided a flat namespace of unit types without a clear extension model, P3045 structures units around base dimensions with explicit conversion paths between systems.

One recurring question in the standardization discussion is scope: should the standard commit to a general physical quantities system covering everything from nanometres to astronomical units, or adopt a more conservative first step? The proposal argues for the former, and the argument is sound. A units library that only covers some dimensions creates gaps where users fall back to raw double, which defeats most of the purpose. The Mars Climate Orbiter was not lost because nobody had thought of type-safe time; it was lost because the dangerous value lived in a domain the type system did not cover.

Why Codebases Still Pass double

The libraries have been available for years. Boost.Units has existed since around 2003. nholthaus/units since 2016. mp-units has been production-ready for a while. The question is why so many codebases still represent distances as float and temperatures as double.

Part of it is inertia. Existing interfaces are built around raw numeric types, and retrofitting a units library into a large codebase requires either a full migration or an awkward period where some values carry units and others do not. That partial state can be worse than neither, because it creates false confidence.

Part of it is fragmentation. Three major libraries with incompatible types mean that adopting units in your code does not compose with units in a third-party library. If the linear algebra library you depend on represents distance as double, your quantity<metre, double> hits a conversion boundary at every call site. The composition problem is real, and it is the strongest argument for standardization.

Once std::quantity<si::metre> exists as the standard type that distance values use, library authors will expose it in their interfaces. The conversion friction disappears, and the safety guarantee propagates across library boundaries. This is the same dynamic that made std::chrono successful: once the standard duration types existed, every API that cared about time precision adopted them.

Where This Leaves You

For new code in domains where unit safety matters, whether that is robotics, physics simulation, navigation, sensor fusion, or financial calculations with currency, adopting mp-units now is defensible. The compiler support is broad (GCC 12+, Clang 16+, MSVC 19.36+), the API is stable, and the P3045 standardization process means the design is unlikely to change dramatically before it lands in the standard.

The original isocpp.org piece by Wu Yongwei is a good tour of what the libraries look like in practice. The mp-units documentation covers the full API including edge cases around dimensionless quantities, mixed-precision arithmetic, and defining custom unit systems.

The Mars Climate Orbiter is the dramatic example, but the common failure is quieter: a function that takes two double parameters, called with the arguments reversed, producing a result that looks plausible for weeks before something in the system behaves incorrectly. The compiler cannot help with raw double. It can help with quantity<metre>. That gap has been closable for a long time; the question is whether standardization finally makes closing it the path of least resistance.

Was this interesting?