mp-units (C++)
The C++ standardization vehicle for quantities and units (P3045): a value-based C++20/23 design — concepts, NTTPs, and consteval symbolic algebra over empty tag types — carrying the field's most developed kind system, in which quantity_spec hierarchies distinguish width from height (both length), torque from energy, and Hz from Bq, beyond what any dimension vector can see.
| Field | Value |
|---|---|
| Language | C++ (C++20 minimum; C++23 explicit this removes the CRTP; C++20 modules and freestanding builds supported) |
| License | MIT |
| Repository | mpusz/mp-units |
| Documentation | mpusz.github.io/mp-units (MkDocs; sources in-tree under docs/) |
| Key authors | Mateusz Pusz (original author), Johel Ernesto Guerrero Peña, Chip Hogg, "The mp-units project team" (CITATION.cff) |
| Category | Library-level compile-time checking (no compiler support; WG21 standardization candidate for C++29) |
| Mechanism | Value-based symbolic expressions: quantity<R, Rep> where the NTTP R binds a quantity_spec (a node in a kind-hierarchy tree) to a unit; dimension/unit/spec algebra runs in consteval over empty tag types |
| Exponent domain | ℚ — rational exponents via power<F, Num, Den> (pow<1, 2>(…), sqrt, cbrt); base-dimension set open (any base_dimension<Symbol>) |
| Checking time | Compile time (concept-constrained overloads + immediate consteval functions); zero runtime checks |
| Analyzed version | d7b11de (pinned clone, 2026-07-02; in-tree version 2.6.0-dev per src/CMakeLists.txt) |
| Latest release | v2.5.0 (2025-12-24, CITATION.cff) |
NOTE
mp-units is this survey's canonical data point for kind-aware dimensional analysis: where every other system stops at the dimension group (or bolts on comparability tags, like uom's Kind), mp-units models the ISO 80000 system of quantities as a first-class tree and computes with it. It is also the survey's C++ standardization story: P1935R2 (2020) → P2980R1 (2023) → P3045R8 (2026), all captured locally. Its C++ siblings are the template-metaprogramming ancestor Boost.Units and the deliberately-smaller Au (whose vector-space unit magnitudes mp-units adopted). See the comparison capstone for the cross-system synthesis.
Overview
What it solves
mp-units gives C++ compile-time dimensional analysis and a stronger property no dimension vector can provide: quantity-kind and quantity-hierarchy safety. The README states the positioning in its opening paragraph (README.md):
"mp-units is a Modern C++ (C++20 and later) library providing the full spectrum of compile-time safety for domain-specific quantities and units, from dimensional analysis to quantity kind safety, built on the ISO 80000 International System of Quantities (ISQ). It is a candidate for C++29 standardization (P3045), your chance to shape the future of C++."
The kind claim is concrete and the README leads with it (README.md):
"Quantity Kind Safety — mp-units pioneered this level: it distinguishes quantities that share the same dimension but represent fundamentally different physical concepts: frequency (Hz) ≠ radioactive activity (Bq), absorbed dose (Gy) ≠ dose equivalent (Sv), plane angle (rad) ≠ solid angle (sr). Dimensional analysis alone cannot catch these errors, mp-units prevents them at compile time."
Function signatures can demand not just a dimension but a specific quantity: void calculate_trajectory(quantity<isq::kinetic_energy[J]> e); accepts a kinetic energy and rejects a potential energy, though both are joules (README.md). The justification is ISO 80000 itself, quoted in the user's guide (docs/users_guide/framework_basics/systems_of_quantities.md L67–80):
"Quantities of the same dimension are not necessarily of the same kind."
Design philosophy
Everything is a value. Dimensions, units, and quantity specs are inline constexpr objects of empty types (detail::SymbolicConstant requires std::is_empty_v — symbolic_expression.h L50–53); all algebra on them is done by consteval functions on those objects, and the types surface only in one place — diagnostics. The convention is stated in the framework headers themselves (dimension.h L136–139):
"A common convention in this library is to assign the same name for a type and an object of this type. Besides defining them user never works with the dimension types in the source code. All operations are done on the objects. Contrarily, the dimension types are the only one visible in the compilation errors. Having them of the same names improves user experience and somehow blurs those separate domains."
Diagnostics are a design goal, not a by-product. The feature list in docs/getting_started/about.md promises being "Optimized for readable compilation errors and great debugging experience", and the library engineers for it: a documented set of expression-simplification rules keeps generated types short (interface_introduction.md L160–240), and truncation errors carry constexpr-formatted English messages (see Diagnostics).
Both the quantity and the unit stay in the type. Unlike uom's eager normalization to base units, a quantity<isq::height[m]> remembers both its place in the ISQ tree and its unit; conversions are lazy and explicit at boundaries. This is also Au's stance — the two libraries co-evolved, and their authors co-author P3045.
The WG21 trajectory
The three locally captured papers mark the arc. P1935R2 (2020, LEWG/SG6/SG18) is the V1-era pitch — "This document starts the discussion about the Physical Units support for the C++ Standard Library" (P1935R2) — reviewed in Belfast 2019 and Prague 2020, where (per P3045's history) the groups "expressed interest in the potential standardization of such a library and encouraged further work" (P3045R8). P2980R1 (2023-11-28) resets after the V2 redesign with a motivation/scope/plan paper: "Having quantities and units support in C++ would be extremely useful for many C++ developers, and ideally, we should ship it in C++29. We believe that it can be done, and we propose a plan to get there" (P2980R1). P3045R8 (2026-05-12, LEWG/SG6/SG16/SG20) is the wording-track omnibus — "This paper describes and defines a generic framework for quantities and units library" — authored jointly by the mp-units developers and "the authors of other actively maintained similar libraries on the market" (P3045R8), i.e. the field's consensus paper rather than one library's.
How it works
One value-carrying class
The whole runtime surface is a single class template with a single data member; every other property is a static constexpr value of an empty type (quantity.h L448–459):
// mp-units: src/core/include/mp-units/framework/quantity.h L448-459 (abridged)
template<Reference auto R, RepresentationOf<get_quantity_spec(R)> Rep = double>
class quantity : public detail::quantity_iface {
public:
Rep numerical_value_is_an_implementation_detail_; ///< needs to be public for a structural type
static constexpr Reference auto reference = R;
static constexpr QuantitySpec auto quantity_spec = get_quantity_spec(reference);
static constexpr Dimension auto dimension = quantity_spec.dimension;
static constexpr Unit auto unit = get_unit(reference);
using rep = Rep;The reference R is built by indexing a quantity spec with a unit — isq::speed[m / s] — or by a bare unit whose definition already names its kind (42 * m is a quantity<si::metre{}, int> of kind_of<isq::length>). A quantity_point<R, Origin, Rep> wraps a quantity plus an origin for affine use (quantity_point.h L507–519).
Units are one-liners; magnitudes are a vector space
Units are empty strong types declaring a symbol, an optional kind, and a magnitude expression (si/units.h L42–95):
// mp-units: src/systems/include/mp-units/systems/si/units.h L42-95 (abridged)
inline constexpr struct second final : named_unit<"s", kind_of<isq::duration>> {} second;
inline constexpr struct metre final : named_unit<"m", kind_of<isq::length>> {} metre;
inline constexpr struct hertz final : named_unit<"Hz", one / second, kind_of<isq::frequency>> {} hertz;
inline constexpr struct becquerel final : named_unit<"Bq", one / second, kind_of<isq::activity>> {} becquerel;
inline constexpr struct gray final : named_unit<"Gy", joule / kilogram, kind_of<isq::absorbed_dose>> {} gray;
inline constexpr struct sievert final : named_unit<"Sv", joule / kilogram, kind_of<isq::dose_equivalent>> {} sievert;
inline constexpr struct degree_Celsius final : named_unit<symbol_text{u8"℃", "`C"}, kelvin, ice_point> {} degree_Celsius;Scale factors are mag<N>, mag_ratio<N, D>, mag_power<Base, Num, Den>, and constants like mag<pi_c> — represented as a vector space over prime-number bases with rational exponents, plus "custom tag" bases for irrationals (bits/unit_magnitude.h L74–93: prime bases as NTTPs, mag_constant types with a long double value for "any irrational base we admit into our representation"). This is the representation pioneered by Au; it keeps km/h → m/s exact, lets π cancel symbolically, and underlies the "faster-than-lightspeed constants" feature (faster_than_lightspeed_constants.md):
"The mp-units library allows and encourages the implementation of physical constants as regular units. With that, the constant's value is handled at compile-time, and under favorable circumstances, it can be simplified in the same way as all other repeated units do."
so speed_of_light_in_vacuum is named_constant<"c", mag<299'792'458> * metre / second> and p / (m * c) strikes c out of the type before any arithmetic happens.
The symbolic expression engine
All three entity families — dimensions, quantity specs, units — share one engine (symbolic_expression.h): a product is a sorted argument list, negative exponents live in a trailing per<...>, and non-unit exponents wrap in power<F, Num, Den>. expr_multiply/expr_divide merge sorted lists; expr_consolidate aggregates equal factors by adding exponents; expr_simplify cancels across numerator/denominator (symbolic_expression.h L188–307, L417–477). The result is a canonical form: A * B and B * A produce the same type, so equality of dimensions is literally type identity (dimension.h L81–84 — operator== returns is_same_v<Lhs, Rhs>). The user's guide documents each simplification rule with before/after tables, explicitly "to keep generated types short and readable" (interface_introduction.md L178–240).
Locally verified round trip
The survey's standard valid-path program, compiled and run against the pinned clone [reproduced locally, g++ (GCC) 15.2.0, -std=c++23, 2026-07-03]:
// locally reproduced — mixed-unit addition; L/T evaluates to speed
quantity l1 = 500.0 * m;
quantity l2 = 1.5 * km;
quantity sum = l1 + l2; // OK: common unit computed (m)
quantity<isq::speed[m / s]> v = sum / (4.0 * s); // OK: length/time -> speed
static_assert(sum.unit == m);
// v.numerical_value_in(m / s) == 500.0 — checked at run time, exit code 0Unlike uom, the mixed-unit sum does not silently live in a base unit: the result unit is the common unit of the operands (here m), computed at compile time, and the integer-overflow implications of scaling are themselves concept-checked (quantity.h L152–187).
Dimension representation
A base dimension is an empty type keyed by a symbol, not a position in a vector (dimension.h L143–146):
// mp-units: src/core/include/mp-units/framework/dimension.h L143-146
MP_UNITS_EXPORT template<symbol_text Symbol>
struct base_dimension : detail::dimension_interface {
static constexpr auto _symbol_ = Symbol; ///< Unique base dimension identifier
};Derived dimensions are the normalized symbolic products described above — speed has dimension derived_dimension<dim_length, per<dim_time>>, accelerationderived_dimension<dim_length, per<power<dim_time, 2>>> (dimension.h L149–191, with a worked list in the doc comment). dimension_one is the empty product (L200–203). Three properties are load-bearing for this survey:
- The exponent domain is
ℚ.power<F, Num, Den>accepts any valid positive rational — negative exponents are carried by theper<…>wrapper with inverted sign (symbolic_expression.hL104–122) — and the dimension interface exposespow<Num, Den>,sqrt,cbrtdirectly on dimension values (dimension.hL96–111). Fractional dimensions are first-class, not an encoding trick — contrast theℤ-onlyuom/dimensionedand F#'sℚ-during-inference (Kennedy's type system). - The base set is open. Any library or user can mint
base_dimension<"X">; there is no 7-slot vector to outgrow. The shipped strong angular system does exactly this —inline constexpr struct dim_angle final : base_dimension<symbol_text{u8"α", "a"}> {} dim_angle;(angular/units.hL39) — giving angle a real dimension for those who want it. In the free-abelian-group model, mp-units implements the group over an open, named generator set withℚexponents, normalized by sorting on type names (type_list_name_less,symbolic_expression.hL330–331). - Dimensions are computed, not declared, for derived quantities. A
derived_quantity_specobtains its dimension by projecting each quantity spec in its expression to its dimension and re-normalizing (expr_map<to_dimension, …>,quantity_spec.hL584–585). The dimension layer is thus a quotient of the richer quantity-spec layer — many specs, one dimension — which is precisely what makes room for kinds.
Checking & inference
The checker evaluates; it never solves. Every operator is a concept-constrained overload whose result type is produced by running the consteval algebra on the operand values: operator* on quantities computes R1 * R2 (quantity.h L316–321), which multiplies the quantity specs (quantity_spec.h L196–210) and the units, normalizing both. Addition and comparison require a common reference to exist: operator+ is constrained by CommonlyInvocableQuantities, which requires HaveCommonReference — requires { mp_units::get_common_reference(R1, R2); } (quantity.h L144–167, L236–245). Everything terminates (it is ordinary overload resolution plus template instantiation over concrete types); in the mechanism taxonomy mp-units sits with uom in the "checker evaluates" row, opposite the AG-unification of F# and uom-plugin.
What is genuinely novel is which common type addition computes. For two quantities of the same kind, the common quantity spec is their lowest common ancestor in the kind tree (systems_of_quantities.md L236–247):
// mp-units: docs/users_guide/framework_basics/systems_of_quantities.md L244-246
static_assert(get_common_quantity_spec(isq::width, isq::height) == isq::length);
static_assert(get_common_quantity_spec(isq::thickness, isq::radius) == isq::width);
static_assert(get_common_quantity_spec(isq::distance, isq::path_length) == isq::path_length);width + height compiles and yields a length — mutually comparable per ISO 80000 — while width + duration has no common spec and fails. Around the tree sits a four-level conversion lattice (systems_of_quantities.md L258–347): implicit up the tree (every radius is a width is a length), explicit down the tree (isq::height(q) — not every length is a height), quantity_cast across branches (height → width: same kind, different branch), and nothing across kinds or dimensions (not even quantity_cast<isq::length>(42 * s) compiles).
Dimensional polymorphism is expressible with zero bound-spelling. Because result types are computed from argument values, a generic sqr : α → α² is just auto sqr(Quantity auto q) { return q * q; } — no per-dimension where-clauses (contrast uom's seven-fold typenum bounds). The idiomatic constrained form uses QuantityOf (generic_interfaces.md L139–166):
// mp-units: docs/users_guide/framework_basics/generic_interfaces.md L164-168 (abridged)
QuantityOf<isq::speed> auto avg_speed(QuantityOf<isq::length> auto distance,
QuantityOf<isq::duration> auto duration)
{
return distance / duration;
}which is checked semantically: the deduced result must be implicitly convertible to isq::speed in the tree, not merely dimension-LT⁻¹. What the model cannot do is run backwards — there is no unification, no principal types, and no inferring an argument's dimension from a required result (Kennedy-style sqrt : α² → α polymorphic inference has no counterpart; sqrt exists but as a value-level consteval function on known operands).
Extensibility
Extension is the library's showcase — the README claims "Custom dimensions, quantities, and units in a single line of code" (README.md), and the shipped systems are built exclusively from the public surface:
- New unit: one line, referencing any existing unit expression —
inline constexpr struct yard final : named_unit<"yd", mag_ratio<9'144, 10'000> * si::metre> {} yard;(yard_pound.hL50). Becauseyardis defined in terms ofsi::metre, yard/metre interop needs no registration: any two units of the same kind convert through their magnitudes. Prefixes are unit templates —template<PrefixableUnit U> struct nano_ final : prefixed_unit<"n", mag_power<10, -9>, U{}> {};(si/prefixes.hL41). - New quantity in a kind tree: one
QUANTITY_SPECline naming the parent —QUANTITY_SPEC(width, length);— optionally with an equation and/oris_kind(systems_of_quantities.mdL204–224; the CRTP-hiding macro is the portable spelling, C++23 needs none of it). - New base dimension / whole system:
base_dimension<"X">plus a rootquantity_spec— the pattern of the strong angular system (angular/units.hL39–43) and of the natural-units system (natural.h, whereelectronvoltis a base unit andc = 1). The ISQ/SI split is architectural:src/systems/isqdefines the system of quantities (the tree — no units at all),src/systems/sithe system of units that references it; CGS, IAU, HEP, imperial/yard_pound, IEC, and typographic systems all reuse the same ISQ tree with different units, so cross-system conversion is just magnitude arithmetic. - Scoping: definitions are ordinary C++ namespaced objects; there is no global registry to collide in (
concepts-checked at use). Two independently defined base dimensions with the same symbol text would be distinct types (symbol is identity for sorting, the type for equality) — collisions surface as failed conversions, not silent merges.
Expressiveness edges
Fractional powers: present and exercised. The dedicated static test builds
nV/√Hz-style amplitude spectral density (T^(1/2)dimension), fracture toughnessMPa·√m(M L^(-1/2) T⁻²), and a cube-root Manning coefficient — asserting, e.g.,unit_symbol(si::mega<si::pascal> * sqrt(si::metre)) == "MPa m^(1/2)"anddimension_symbol(...) == "ML^-(1/2)T⁻²"(test/static/fractional_exponent_quantity.cppL40–95). This is the survey's clearest demonstration thatℚexponents pay off in real quantities — the exact idiomuomcannot write at all.Affine quantities: a full framework, not a temperature special-case.
quantityis the displacement vector,quantity_pointthe point; the guide enumerates the affine operations and their prohibitions — "It is not possible to: add two points, subtract a point from a vector, multiply nor divide points with anything else" (the_affine_space.mdL34–41). Origins are typed:absolute_point_origin<QS>roots a frame,relative_point_origin<Point>chains compile-time offsets, andsi::kelvincarriesabsolute_zerowhiledegree_Celsiusis defined againstice_point(relative_point_originat exactlypoint<milli<kelvin>>(273'150)—si/units.hL86–89). The multiply syntax is deliberately disabled for temperature units (ambiguous point-vs-delta);delta<deg_C>(3)andpoint<deg_C>(20)disambiguate. Distinct absolute origins are bridged by an explicitframe_projectioncustomization point (the_affine_space.mdL407–430). This is the survey's most complete realization of the torsor model.Logarithmic quantities: absent, and acknowledged. The SI units file carries the honest marker (
si/units.hL119–122):cpp// mp-units: src/systems/include/mp-units/systems/si/units.h L119-122 // TODO the below are logarithmic units - how to support those? // neper // bel // decibelNo
dBsupport exists anywhere in the pinned clone — the same gap as nearly every system here except Pint.Angles: both answers, user's choice. In the default SI/ISQ model angular measure is a dimensionless subkind (
is_kindoff the dimensionless tree), soradandsrare distinct from each other and from bare ratios but erase to one in dimension. The optional strong angular system instead gives angle a genuine base dimensionαwithradian = named_unit<"rad", kind_of<angle>>(angular/units.hL39–47). Shipping both — with the ISQ-conforming one as default — is unique in this survey.Kind vs dimension: the system's crown jewel. Three
T⁻¹kinds (frequency, activity, modulation rate),GyvsSv, torque vs energy — each is a separate tree root or subkind, and the user's guide motivates each pair before defining the machinery (systems_of_quantities.mdL15–80). Within a kind, the hierarchy refines further (the length tree: width/altitude/wavelength/…; the energy tree: mechanical/potential/kinetic/…). Kinds propagate through arithmetic instead of eroding: operations onkind_ofvalues stay kinds (kind_of<isq::length> / kind_of<isq::duration>iskind_of<isq::length / isq::duration>), while mixing a kind with a strong quantity produces the strong quantity (systems_of_quantities.mdL449–462) — contrastuom, whose kinds erase to the default under every×/÷. Subkinds viais_kind(e.g. fluid head vs water head, both height) inherit the parent's unit and dimension yet stay mutually incomparable (systems_of_quantities.mdL477–560).Residual honesty: the ISO hierarchy itself is admitted to be "to some extent, arbitrary" (ISO/IEC Guide 99, quoted at
systems_of_quantities.mdL355–359), and the V2 tree reverses ISO's height/altitude parentage as a documented workaround for signed coordinates, slated for a V3point_formechanism (systems_of_quantities.mdL110–125). Kind modeling has judgment calls in it; mp-units documents its own.
Zero-cost story
The claim is structural, stated as "Zero space overhead for high-level abstractions" and "Performance on par with (sometimes even better than) fundamental types" (about.md, README.md). The evidence in the clone:
- One data member, everything else empty.
quantity<R, Rep>stores exactlyRep numerical_value_is_an_implementation_detail_(quantity.hL451);Ris an NTTP value of an empty type (SymbolicConstantdemandsstd::is_empty_v—symbolic_expression.hL50–53). Locally verified against the pinned clone:sizeof(quantity<si::metre>) == sizeof(double)andsizeof(quantity<si::kilo<si::metre>, int>) == sizeof(int)bothstatic_assertclean [reproduced locally,g++ 15.2.0, 2026-07-03]. - The symbolic algebra cannot survive to runtime by construction —
expr_multiply,pow,get_common_quantity_specetc. areconsteval(immediate) functions; there is no code path on which dimension bookkeeping could execute at runtime. - Lazy conversion. Values stay in their declared unit; scaling happens only at explicit boundaries (
.in(unit),value_cast, construction of a differently-referenced quantity), with the conversion factor folded at compile time (value_conversions.mdL225–261). Constants-as-units go further: repeated constants cancel in the type, so the multiplication is never emitted (ftldoc). - The measured exception. Mixed-unit comparison/addition of integer representations routes through a double-width integer scaling path to avoid overflow (
compare_quantities,quantity.hL189–226) — deliberate correctness work a rawintcomparison would not do, and concept guards (overflows_non_zero_common_values) reject cases that cannot be done safely at all. - No benchmark suite ships in the pinned clone —
test/runtimeis functional tests only. The performance claims rest on the structural argument plus Compiler Explorer links in the docs (README.mdlinks a live godbolt example), not on an in-repo measured corpus. That is thinner published evidence than the claim's prominence suggests, even if the structural case is strong.
Diagnostics
The mandated experiment — adding metres to seconds — against the pinned clone (headers included with -I src/core/include -I src/systems/include):
// locally reproduced — mismatch.cpp
quantity<si::metre> l = 1.0 * si::metre;
quantity<si::second> t = 1.0 * si::second;
auto err = l + t;mismatch.cpp: In function ‘int main()’:
mismatch.cpp:9:16: error: no match for ‘operator+’ (operand types are
‘mp_units::quantity<mp_units::si::metre()>’ and ‘mp_units::quantity<mp_units::si::second()>’)
9 | auto err = l + t;
| ~ ^ ~
| | |
| | quantity<mp_units::si::second()>
| quantity<mp_units::si::metre()>
mismatch.cpp:9:16: note: there are 4 candidates[reproduced locally, g++ (GCC) 15.2.0, -std=c++23, 2026-07-03; long compiler lines re-wrapped, content verbatim]
The headline is the survey's best-in-class: the operand types read in the domain's language — quantity<mp_units::si::metre()> vs quantity<mp_units::si::second()> — with no exponent encodings, no eight-slot trait-object dumps, no side files (contrast the uom error quoted in that page's Diagnostics). The full output is 80 lines: GCC then walks the four operator+ candidates, and the binary-candidate trail ends at the exact semantic reason (paths abbreviated, lines re-wrapped):
.../framework/quantity.h:145:9: required for the satisfaction of
‘HaveCommonReference<Q1::reference, Q2::reference>’
[with Q1 = mp_units::quantity<mp_units::si::metre{}, double>;
Q2 = mp_units::quantity<mp_units::si::second{}, double>]
.../framework/quantity.h:145:72: note: the required expression
‘mp_units::get_common_reference(R1, R2)’ is invalidThe kind-safety failure gives the same quality of headline [reproduced locally, g++ 15.2.0, 2026-07-03]:
kinds.cpp:8:19: error: no match for ‘operator+’ (operand types are
‘mp_units::quantity<mp_units::si::hertz(), int>’ and ‘mp_units::quantity<mp_units::si::becquerel(), int>’)
8 | auto q = 1 * Hz + 1 * Bq; // same dimension T^-1, different kindsThree engineered mechanisms produce this quality. First, the same-name type/object convention quoted above means the type in the error is the name the user wrote. Second, the expression-template simplification rules exist explicitly "to keep generated types short and readable" (interface_introduction.md L178) — a derived unit prints as derived_unit<si::metre, per<si::second>>, not a nest of aliases. Third, for constraint failures with a reason (truncation, overflow), concepts embed constexpr-formatted English via unsatisfied<"…"> — e.g. unsatisfied<"Conversion from '{}' as '{}' to '{}' as '{}' is truncating">(unit_symbol(FromUnit), …) (quantity.h L98–102); under compilers with constexpr exceptions (__cpp_constexpr_exceptions, auto-enabled — bits/hacks.h L159–161) the message is thrown inside the consteval evaluation (bits/unsatisfied.h L63–67), so the compiler prints the formatted sentence as part of the error. This is the most deliberate diagnostics engineering in the survey.
Ergonomics & compile-time cost
Declaration overhead is minimal at every tier. Using the library is #include <mp-units/systems/si.h> plus using namespace si::unit_symbols;; a new unit, quantity, prefix, or base dimension is one line each (grounded above). The library deliberately offers two commitment levels — simple (quantity<km / h>, unit-only, kind-checked) and typed (quantity<isq::speed[km / h]>, tree-checked) — so ISQ rigor is opt-in per interface (simple_and_typed_quantities.md). UDLs are rejected by design with documented reasons (literals-only, widest-type defaults, namespace pollution — faq.md L10–46); the multiply syntax 42 * m works for variables and any representation type.
Error readability is quoted above; the one caveat is volume — the readable two-line headline is followed by ~78 lines of candidate/constraint notes, and GCC suggests -fconcepts-diagnostics-depth=2 for the deeper "why" of a constraint failure.
Compile-time cost is real but moderate, and modules are the sanctioned fix. Locally, compiling a single TU that includes <mp-units/systems/si.h> + <mp-units/systems/isq.h> takes 3.6 s wall (g++ 15.2.0 -std=c++23 -c, header mode, 2026-07-03) — each TU pays it in header mode. The project ships C++20 modules (import mp_units;, clang 17+ per the compiler-support matrix) precisely to amortize this, and documents per-feature macro switches (MP_UNITS_API_NO_CRTP, MP_UNITS_API_THROWING_CONSTRAINTS) that trade portability against interface quality (bits/hacks.h L150–163). Freestanding builds (MP_UNITS_HOSTED=0) strip the text/formatting layer for embedded use.
Strengths
- The kind system — quantity hierarchies with LCA-based addition, four-level conversion lattice,
kind_ofpropagation, andis_kindsubkinds; catchesHz/Bq,Gy/Sv, torque/energy, width/height misuse that every dimension-vector system in this survey admits. ℚexponents over an open generator set —nV/√HzandMPa·√mare first-class, test-covered quantities; new base dimensions are one-liners.- Best-in-survey diagnostics — domain-language types in errors by deliberate engineering (same-name convention, simplification rules,
unsatisfiedformatted messages). - Full affine-space model — typed origins, absolute/relative/frame-projected, temperature done via origins rather than special cases; the most complete torsor realization surveyed.
- Vector-space unit magnitudes — exact rational/irrational scale factors, symbolic
π, constants-as-units that cancel in the type. - Structurally zero-cost — one
Repmember, empty NTTP tags,constevalalgebra; locally verified size identities. - Standardization gravity — P3045 is co-authored across competing libraries; the design documented here is the likely shape of
stdquantities in C++29.
Weaknesses
- C++20/23 floor with real compiler sensitivity — CRTP fallbacks, clang-version workarounds, and MSVC ICE shims lace the headers (
unit_magnitude.hL76–91); older toolchains get a degraded interface (noexplicit this, no throwing constraints, no modules). - No logarithmic quantities —
dB/Npare an openTODOin the SI system itself. - Kind hierarchies embed contestable judgments — ISO's own "to some extent, arbitrary" caveat applies; the V2 height/altitude inversion shows the tree can disagree with the standard it models, and downstream code inherits those calls.
- No inference — the checker evaluates only; nothing like Kennedy principal types or
uom-pluginunification; generic code is concept-constrained forward-only. - Compile-time weight in header mode — seconds per TU; modules help but narrow the supported-compiler set further.
- Performance claims outrun in-repo evidence — no benchmark suite in the pinned clone; the zero-cost case is structural plus external godbolt links.
- Sheer conceptual surface — quantity spec vs kind vs dimension vs unit vs reference vs point origin is a five-layer ontology; the docs are excellent but long, and P3045's own teachability chapter segments its audiences for a reason.
Key design decisions and trade-offs
| Decision | Rationale | Trade-off |
|---|---|---|
Value-based API: entities are constexpr objects, algebra is consteval | Natural syntax (m / s, pow<2>(m)); types appear only in diagnostics; same-name convention makes them readable | Requires C++20 NTTP/concepts machinery; heavy header instantiation cost per TU |
Kind tree (quantity_spec hierarchy) above the dimension group | Catches same-dimension misuse (Hz/Bq, torque/energy); LCA gives meaningful width + height | Tree contents are judgment calls (ISO: "to some extent, arbitrary"); five-layer ontology to learn |
Symbolic expressions with ℚ exponents, canonicalized by sorting | √Hz-class quantities expressible; type identity = structural equality with no fixed base-vector arity | Type names grow with expression complexity; canonical order is by type name, not physics convention |
| Unit kept in the type; lazy, explicit conversions | No boundary precision loss; integer reps stay exact; conversion cost visible in code | Mixed-unit integer comparisons need double-width scaling; common-unit computation adds concept complexity |
Vector-space magnitudes (prime + irrational bases, ℚ exponents) | Exact km/h→m/s; symbolic π; constants-as-units cancel before arithmetic | Magnitude machinery is intricate (MSVC ICE workarounds); long double constant values cap irrational precision |
Diagnostics engineered as a feature (unsatisfied, simplification rules) | Survey-best error headlines in the domain's vocabulary | Full errors still ~80 lines of candidate notes; best messages gated on constexpr-exceptions compilers |
| ISQ/SI split: systems of quantities separate from systems of units | One quantity tree serves SI/CGS/IAU/HEP/imperial; cross-system conversion is pure magnitude arithmetic | Users must grasp the quantity/unit distinction before their first typed interface |
| WG21 standardization as the explicit end-state | API stability pressure, multi-library consensus (P3045 co-authors), field-wide review | Design conservatism (portability macros, CRTP fallbacks) and paper-driven feature pacing |
Sources
- mpusz/mp-units — GitHub repository (pinned locally at
$REPOS/cpp/mp-units@d7b11de, 2026-07-02) framework/quantity.h—quantityclass, operator constraints,unsatisfiedtruncation messages, double-width comparisonframework/dimension.h—base_dimension,derived_dimension,pow<Num, Den>/sqrt/cbrt, type-identity equality, same-name convention noteframework/symbolic_expression.h—per/power<F, Num, Den>, consolidation, simplification,expr_multiply/expr_pow/expr_mapframework/quantity_spec.h—quantity_specspecializations, kind interface,kind_of, dimension projectionframework/quantity_point.h—quantity_point,absolute_point_origin/relative_point_originbits/unit_magnitude.h— vector-space magnitudes over prime/irrational bases ·framework/unit_magnitude.h—mag/mag_ratio/mag_power/pi_cbits/unsatisfied.h— consteval-throw diagnostic messages ·bits/hacks.h— feature gates (NO_CRTP,THROWING_CONSTRAINTS)systems/si/units.h— SI unit definitions, Celsius origins, logarithmic-units TODO ·systems/si/prefixes.h·systems/angular/units.h— strong angle base dimension ·systems/yard_pound.htest/static/fractional_exponent_quantity.cpp—√Hz,MPa·√m, cube-root quantities- Docs: systems of quantities (kind trees, conversion lattice,
is_kind) · the affine space · interface introduction (simplification rules) · generic interfaces · faster-than-lightspeed constants · value conversions · simple vs typed quantities · about (feature claims) · FAQ (no UDLs) · compiler support README.md— positioning, kind-safety pitch, key features ·CITATION.cff— authors, release ·src/CMakeLists.txt— in-tree version- WG21 captures (local:
$PAPERS/mpusz-{2020-p1935r2,2023-p2980r1,2026-p3045r8}-*.html): P1935R2 — "A C++ Approach to Physical Units" · P2980R1 — motivation/scope/plan, C++29 target · P3045R8 — "Quantities and units library" - Local reproductions (mismatch + kind errors, valid ops,
sizeofasserts, compile timing): scratch workspace against the pinned clone,g++ (GCC) 15.2.0,-std=c++23, 2026-07-03 - Related deep-dives in this survey: type-system mechanisms · Kennedy's type system · free abelian group · torsors & affine quantities ·
uom·dimensioned· F# units of measure ·uom-plugin· Boost.Units · Au · Pint ·Unitful.jl· the comparison capstone