· 6 min read ·

C++ Unit Safety Has Been a Solved Problem for Years. The Standard Is Just Catching Up.

Source: isocpp

In 1999, NASA lost a $327.6 million spacecraft because one engineering team produced thruster data in pound-force seconds and another consumed it expecting newton seconds. No compiler caught the mismatch. No runtime check fired. The Mars Climate Orbiter entered the Martian atmosphere at the wrong angle and disintegrated.

That story gets told often in software safety circles, and the lesson is always the same: your type system should make this class of error impossible to express. C++ has had the tools to do this for years. Wu Yongwei’s recent writeup on isocpp.org catalogs the current landscape in response to P3045R7, the proposal to bring physical units into the C++ standard library. It is a useful survey. But the more interesting story is what the libraries built before this proposal reveal about the design space, and why the distinction between “units” and “quantities” turns out to matter more than it first appears.

What std::chrono Already Teaches Us

The C++ standard already contains a well-designed unit system. std::chrono has been shipping since C++11, and its type model is instructive. duration is parameterized on both a representation type and a ratio, so milliseconds and seconds are distinct types. Adding them works and produces the correct result through implicit conversion. Adding two time_point values does not compile, because the algebra of the affine space does not permit it.

auto t1 = chrono::steady_clock::now();
this_thread::sleep_for(500ms);
auto t2 = chrono::steady_clock::now();
auto duration = t2 - t1;   // compiles: point minus point = duration
auto bad = t1 + t2;         // does not compile: point plus point is nonsense
cout << duration / 1.0ms;   // outputs ratio as double

This is not a trivial design. The time_point and duration separation encodes an algebraic fact about time: you can subtract two instants to get a duration, but you cannot add them. chrono also handles unit conversion automatically when it is lossless, and requires an explicit cast when truncation would occur. duration_cast<milliseconds>(some_seconds) is not pretty, but it forces you to acknowledge the precision loss.

The lesson from chrono is that the hard part of unit safety is not the units themselves. It is the operations, the conversions, and the distinctions between types that share a dimension but represent different physical concepts.

The Pre-Standard Library Landscape

Before mp-units and P3045, developers had two serious options.

Boost.Units has existed for over a decade and provides zero-overhead dimensional analysis through template metaprogramming. It is comprehensive and correct. It is also verbose in ways that discourage adoption. The type names in error messages are long enough to be genuinely difficult to parse, and the syntax for common operations requires more boilerplate than most engineers tolerate in production code.

nholthaus/units addressed the usability gap. It is a single-header library targeting C++14 with no dependencies. The syntax is clean:

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

auto dist  = 20.0_m;
auto vel   = dist / 5.0_s;  // result type: meters_per_second
auto force = 50.0_N;
auto power = force * vel;    // result type: watts

Dimensional analysis is checked at compile time. Multiplying meters by seconds gives meter-seconds, not a compilation error, because that is a valid physical quantity even if you rarely need it. What is prevented is assigning that result to a variable expecting plain meters.

For many projects, nholthaus/units is still the right choice. It works with C++14 compilers, requires no build system integration beyond dropping a header, and produces reasonably readable error messages for common mistakes.

What mp-units Gets Right That Earlier Libraries Missed

mp-units is a different category of library. It requires C++23 for its current 2.4.0 release and serves as the reference implementation for P3045R7. The authors, led by Mateusz Pusz, went further than compile-time dimensional checking and introduced a distinction that earlier libraries collapse: the difference between a unit and a quantity specification.

In every prior library, if two things have the same dimension, they are the same kind of thing. A meter is a meter. Speed is speed. But physical reality is more nuanced. The ISO 80000 series of standards, which formalizes the International System of Quantities (ISQ), distinguishes between quantities that share a dimension but represent different physical concepts. Width, height, depth, and diameter are all lengths. But width and height are not the same thing, and certain assignments between them are semantically wrong even though they are dimensionally consistent.

mp-units encodes this hierarchy:

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

quantity<isq::width[m]>  w = 1 * m;
quantity<isq::height[m]> h = 1 * m;
quantity<isq::length[m]> l = 1 * m;

l = w;  // OK: width is a kind of length
l = h;  // OK: height is a kind of length
w = h;  // ERROR: width is not height

This is not academic pedantry. In aerospace, navigation, and structural engineering, confusing coordinate axes is a real failure mode. A function that accepts isq::altitude cannot silently receive isq::depth even though both are measured in meters. The compiler enforces the distinction.

The library also draws a clean line between simple and typed quantities. Simple quantities carry only unit and representation information, which is sufficient for most arithmetic. Typed quantities carry a full quantity specification from the ISQ hierarchy, enabling the stricter checks above. You choose the level of rigor appropriate to your domain.

The Performance Question

A common concern with these libraries is compile-time and runtime overhead. The zero-overhead claim holds in practice. At runtime, a quantity<si::metre> wrapping a double compiles to the same machine code as a bare double in optimized builds. The type information exists only for the compiler.

Compile times are a different matter. Boost.Units is notoriously slow to compile. nholthaus/units is better due to its simpler architecture. mp-units 2.3 introduced experimental C++ module support, tested across GCC, Clang, and MSVC, which significantly reduces header parsing overhead in large codebases. Module support is one of the concrete benefits of requiring C++23; the library can exploit the module system rather than fighting header inclusion costs.

P3045R7 and the Road to C++26

P3045R7, authored by Pusz, Berner, Guerrero Peña, Hogg, Holthaus, and Michaels, was last revised in January 2025 and targets the C++26 planning horizon. The proposal is unusually thorough. It covers not just the type system design but quantity point arithmetic, affine space modeling for temperatures and timestamps, formatting support via std::format, and the relationship between simple and typed quantities.

The inclusion of Holthaus, the author of nholthaus/units, as a co-author is notable. His library took the pragmatic path; mp-units took the principled path. Their collaboration on the standard proposal suggests the intent is to find a design that is both correct and usable by engineers who are not type theorists.

Standardization matters for more than convenience. Third-party libraries that accept physical quantities currently have no shared vocabulary type. A physics engine and a navigation library cannot exchange a quantity<si::metre / si::second> without including each other’s unit library or wrapping everything in raw doubles. A standard type resolves this the same way std::string resolved the proliferation of string types in the C++98 era.

The Practical Takeaway

For new projects on C++23 or later, mp-units 2.x is the natural starting point if your domain involves physical quantities with any complexity. The ISQ hierarchy catches a class of bugs that purely dimensional analysis misses, and the library is actively maintained against the evolving P3045 proposal. When the standard ships, migration should be mechanical.

For projects on C++14 or C++17, nholthaus/units remains worth the dependency. The single-header format is genuinely convenient, and compile-time dimensional checks catch the most common mistakes.

For neither library, the usual argument is that the code is too slow or the types are too hard. The real reason is that most codebases carry physical quantities as raw double values with comments explaining the expected unit. That is documentation, not enforcement. The compiler cannot read comments.

The machinery to do better has existed in various forms since Boost.Units in the mid-2000s, and the std::chrono design demonstrated over a decade ago that type-safe units integrate naturally into standard C++. P3045R7 is the committee catching up to what library authors have known for years. Whether it lands in C++26 or slips to a later revision, the design space is well-understood, and the implementation quality in mp-units is production-ready today.

A spacecraft was lost because two teams used different units and no tool flagged the mismatch at the boundary. That failure mode is preventable. The tools exist. Using them is a choice.

Was this interesting?