Absolute Quantities
Theme
Contents

Absolute Quantities

Absolute Quantities

Document #: P4185R2 Latest Status
Date: 2026-10-23
Project: Programming Language C++
Audience: SG6 Numerics
Reply-to: Mateusz Pusz
<>

1 Revision history

1.1 Changes since [P4185R1]

R2 proposes absolute quantities, and the title changes from “Completing the Mathematical Model for C++ Quantities and Units” to “Absolute Quantities”. At Brno 2026, SG6 encouraged further work on Section 6 “Absolute quantities” of [P4185R1], and the other chapters moved to papers of their own so that each can be decided separately. The only dependency of this paper is [P3045R10].

1.2 Changes since [P4185R0]

2 Abstract

[P3045R10] models quantities with two abstractions, a delta and a point, both spelled with the quantity class template: quantity<R> is a delta and quantity<point<R, PO>> a point. Neither fits the most common value in a physics equation, an amount measured from a true zero, such as a mass of 5 kg or a thermodynamic temperature of 300 K. A delta has no zero anchor, and two points can’t be added, and a point can’t be multiplied or divided.

This paper proposes a third abstraction, the absolute quantity, anchored at a true zero, and makes it the default meaning of quantity<R>. That is a breaking change to [P3045R10], so SG6 is asked for a direction before the initial API is fixed. Final wording follows implementation.

3 Introduction

[P3045R10] spells a delta quantity<R> and a point on a measurement scale, measured from an explicit or implicit origin, quantity<point<R, PO>>. The two abstractions come from the affine space model: a point minus a point is a delta, a point plus a delta is a point, and the sum or product of two points is ill-formed.

Most values in physics and engineering equations are neither. A mass of 5 kg, a duration of 3 s, or a thermodynamic temperature of 300 K is an amount on a ratio scale: it is measured from a true physical zero, so sums, products, and ratios of such values are meaningful, which they are not for points. Unlike a delta, the value is not a difference between two states. With two abstractions, a user has to choose one of them, and neither describes such a value. Motivation and scope shows where that hurts: the rules for temperature units make the ideal gas law hard to write correctly, and a function signature can’t tell a total mass from a mass loss.

The goal is a library where the ideal gas law

quantity R_calc = P * V / (n * T);

compiles with T written as 28. * K, as an absolute thermodynamic temperature, while an offset-scale reading still has to be converted explicitly, and where mass and mass_loss are distinct types the compiler prevents from being accidentally swapped.

Most code needs only absolute quantities and deltas, since subtracting two absolutes yields a delta. Points stay for offset scales (temperature scales, time zones, reference frames) and for readings relative to a chosen origin.

The three-way split follows the convex-space model of Oliver Rosten [Rosten2025], and it is also implemented, with a different API, in his Sequoia library [Sequoia]. No mainstream units library in any programming language, including F# Units of Measure, Haskell’s dimensional package, or Python’s Pint, distinguishes absolute quantities, deltas, and points as three separate types.

Absolute quantities are implemented on a prototype branch off the version 3 line of [mp-units] (see Implementation experience).

4 Motivation and scope

A quantity<isq::mass[kg]> in [P3045R10] is a delta: it can be negated freely, and its type carries no notion of a physically meaningful zero. This section shows two use cases where that forces users into awkward workarounds.

4.1 The temperature trap

By far the most frequently reported usability issue with the current [P3045R10] design involves temperature. The root cause is the interaction between offset units (°C, °F) and the affine space model.

Consider the ideal gas law, PV=nRTPV = nRT. A user might naively attempt:

auto P = 1. * atm;
auto V = 1. * L;
auto n = 1. * mol;
auto T = 28. * deg_C;                    // ✗ does not compile in P3045!
auto R_calc = P * V / (n * T);

This code does not compile under [P3045R10]. The multiply syntax (28 * deg_C) was initially allowed in earlier revisions but was disabled after feedback from Tiago Freire, who demonstrated the danger with a concrete ideal gas law example. The fix was to disable the multiply syntax for all temperature units, preventing any of them from being used in a value * unit expression. The implementation mechanism for this is that every temperature unit using a point origin in its definition is caught by the restriction; Kelvin was therefore defined with absolute_zero as its point origin specifically to bring it under the same rule, ensuring that 28 * K does not compile under [P3045R10] either.

The inclusion of Kelvin is necessary for a consistent and safe rule. The multiply syntax produces a delta, so 28 * K would not itself construct a point. For Kelvin this causes no direct harm, but the situation changes entirely if the unit in such an expression were later changed to an offset unit (e.g., deg_C). The result would become a meaningless delta rather than a compile error. A uniform rule (no multiply syntax for any temperature unit) is the only way to make the restriction predictable and refactoring-safe.

Nevertheless, this is fundamentally an ad-hoc patch applied on top of the delta/point model rather than a principled solution from measurement theory. It is the right fix for the model as it stands, but it reveals a deeper inadequacy: the model has no way to express “an absolute thermodynamic temperature of 28 K” directly. The constraint is well-motivated to someone who understands the affine space chain, but may appear over-constraining to others.

Users must explicitly choose between a delta and a point:

auto T1 = delta<K>(28);                  // a temperature difference of 28 K
auto T2 = point<K>(28.);                 // 28 K on the Kelvin scale (= 28 K from absolute zero)

Neither option correctly captures the intent. By using delta<K>, T1 is typed as a delta, a signed difference between two temperatures, which is semantically misleading in PV=nRTPV = nRT, where 28 K is an absolute thermodynamic temperature, not a difference between two temperatures. The point T2 carries the right physical meaning but cannot be multiplied or divided, as point arithmetic in an affine space forbids those operations.

The workaround that produces a correct result requires constructing a point measured in Kelvin and extracting the delta from absolute zero:

auto temp = point<deg_C>(28.);
auto T = temp.in(K).delta_from_zero();       // 301.15 K — correct but cumbersome
auto R_calc = P * V / (n * T);

While the affine space model can handle this case, the ergonomics are poor: point<deg_C>(28.).in(K).delta_from_zero() is a far cry from what a domain expert would consider natural. The need to go through a point, convert units, and then extract the delta from zero is a significant usability burden that discourages correct usage.

An alternative approach, suggested by Chip Hogg as a “nature-based constant” idiom, is to subtract absolute_zero directly from the point, obtaining the delta from the physical zero:

auto temp = point<deg_C>(28.);
auto T = temp - absolute_zero;                    // 301.15 K — delta from absolute zero
auto R_calc = P * V / (n * T);

This works even when the point is expressed in deg_C: the subtraction of the absolute zero origin yields a delta whose value is the absolute thermodynamic temperature, and the result can always be scaled to whatever unit is needed. However, this idiom still requires users to know that absolute_zero exists, understand why it must be subtracted, and remember to apply it every time an absolute temperature is needed. This is a teaching burden that grows with every new domain that has an analogous pattern.

This problem is not limited to temperature. It affects any domain where absolute quantities (as opposed to differences) are the natural operands in physical equations. The hardware voltage example in the [mp-units] documentation illustrates the same pattern in embedded systems: an ADC maps a physical voltage range to integer counts, and the measured voltage is an absolute scalar quantity, not a difference. Users have independently requested the same ergonomic improvements for such cases (see mp-units discussion #606). The solution, described in Temperature revisited, makes absolute_zero the natural origin of thermodynamic temperature, so that the kelvin isn’t an offset unit and 28 * K creates an absolute thermodynamic temperature directly, without any conversion idiom.

4.2 Mass balance and accumulation

In process engineering, it is common to track the total mass of material in a vessel, then compute a percentage loss:

quantity<percent> moisture_loss(quantity<kg> water_lost, quantity<kg> total)
{
  MP_UNITS_EXPECTS(total > 0 * kg);
  return water_lost / total;
}

Here, water_lost is a difference (delta) and total is an absolute amount, but the type system makes no distinction: both are quantity<kg>. A user could accidentally swap them:

quantity result = moisture_loss(total_initial, total_initial - total_dried);
// Compiles. Wrong.

One might attempt to address this by defining specialized quantities within the isq::mass hierarchy, for example, total_mass and mass_loss as children of isq::mass. The library’s quantity hierarchy does support this: specialized child quantities of the same kind can be subtracted and compared, but conversion from a parent quantity to a child requires an explicit cast. This would indeed prevent accidental swapping of the two arguments, since total_mass and mass_loss would be distinct types. However, it also means that the legitimate computation total_initial - total_dried yields a total_mass result, not a mass_loss, and passing it to mass_loss would require an explicit conversion. The workaround thus introduces friction for every correct call site, not just the incorrect ones.

However, the distinction here is not between different specializations of mass. Both water_lost and total may even be measured with the same instrument, but the measurement setup is fundamentally different: total is a direct ratio-scale reading anchored at true zero (put the wet material on the scale and read the mass), while water_lost is derived as the difference between two such readings. The difference is in their role: one is a signed difference (delta) and the other is an absolute amount measured from true zero. Encoding every such role as a separate quantity in the hierarchy would cause the quantity tree to proliferate with domain-specific entries that have no basis in measurement science (ISO 80000 does not distinguish “total mass” from “mass loss”: both are simply mass). Every domain would need its own set of role-based quantity specializations, and the hierarchy would grow without bound.

If instead the library distinguished absolute quantities (amounts measured from a true zero) from deltas (signed differences), the function signature alone would enforce the correct usage without polluting the quantity hierarchy. The solution is shown in Mass balance revisited.

5 Absolute quantities

5.1 Overview

We propose introducing a third quantity abstraction, the absolute quantity, that sits between deltas and points in the type hierarchy:

Feature
Point
Absolute
Delta
Multiplication / Division ✗ ✓ ✓
Addition (A + A) ✗ ✓ ✓
Subtraction (A − A) ✓ ✓ ✓
Physical origin Point origin (explicit or implicit) Anchored at true zero N/A
Can use offset units ✓ ✗ ✓
Text output only at the default origin ✓ ✓

Absolute quantities are anchored at a physically meaningful true zero. They cannot use offset units (because they must remain anchored to absolute zero, not a conventional origin). They may be negative where the physics allows it (see Sign of an absolute quantity).

[ Note: An absolute and a delta share the same mathematical structure: both are vector spaces with a distinguished zero element. The type distinction encodes the physical interpretation. An absolute is a ratio-scale measurement anchored at true physical zero, and a delta is a change with no physical zero anchor. Where the values of a quantity can’t be negative, its absolutes form a convex cone [0, +∞), which is closed under addition but not under negation, and is therefore not a vector space. — end note ]

This three-way split mirrors the hierarchy found in measurement theory and physics [Rosten2025]:

Concept
Measurement Scale
Mathematical Structure
Physical Meaning
Point Interval Scale Affine Space A reading relative to an explicit origin (e.g., altitude above sea level, a timestamp)
Absolute (any sign permitted) Ratio Scale Vector Space (real line); zero = true physical zero A ratio-scale quantity whose sign is unconstrained (e.g., electric charge, reactive power)
Absolute (no negative values) Ratio Scale Absolute Convex Space: convex cone [0,+∞)[0, +\infty) with distinguished origin A non-negative amount; true zero is physically meaningful
Delta — Vector Space A change or an interval

The two absolute rows describe the physics of a quantity. The type of an absolute is the same for both, and this paper doesn’t enforce the sign (see Sign of an absolute quantity).

Points on a scale with a non-trivial lower bound but no distinguished absolute origin occupy a fourth abstraction in [Rosten2025]: the convex space, a half-line whose lower bound is fixed but not zero. The Celsius scale is the canonical example: ice_point is a relative_point_origin offset by 273.15 K from si::absolute_zero, the natural origin of thermodynamic temperature (see Temperature revisited), so the lower bound of 0 K corresponds to −273.15 °C on the Celsius scale. This paper models such scales with points.

5.2 Syntax

With absolute quantities as the default, the API becomes:

quantity q1 = 20 * kg;        // absolute quantity (measured from true zero)
quantity q2 = delta<kg>(20);  // delta quantity (signed difference)
quantity q3 = point<kg>(20);  // point quantity (relative to an origin)

producing:

static_assert(std::is_same_v<decltype(q1), quantity<kg, int>>);
static_assert(std::is_same_v<decltype(q2), quantity<delta<kg>, int>>);
static_assert(std::is_same_v<decltype(q3), quantity<point<kg>, int>>);

This mirrors the way physicists write equations: m=10kgm = 10\ \mathrm{kg} denotes an absolute mass, with an explicit Δ\Delta only when a difference is intended. Please note that making quantity default to absolute is a breaking change relative to [P3045R10], where quantity is a delta.

A bare unit means an amount. 20 * kg is an absolute of the quantity kind kind_of<isq::mass>, and it is an absolute whatever its sign. A signed value of a kind, such as a change, is spelled as a delta: delta<m>(-5.) is a displacement of 5 m in the negative direction, and delta<K>(-5.) is a temperature change. A position on a scale is spelled as a point and picks an origin. If [P4411R0] is adopted, a quantity kind takes the domain of its root, so -5. * m, -1 * kg, and -5. * K are contract violations, while -5. * A isn’t, since an electric current can be negative (see Sign of an absolute quantity).

5.3 Absolute quantities are not points

Is the absolute abstraction really necessary, or could we simply give points the ability to be added and multiplied?

We cannot, and the reason lies in the distinction between interval scales and ratio scales in measurement theory.

An affine point lives on an interval scale:

Timestamps, calendar dates, and geographic positions are interval-scale: it makes no sense to add Tuesday to Friday, or to ask what fraction Berlin is of Paris.

An absolute quantity lives on a ratio scale:

So an absolute quantity is not “a point with arithmetic added on top”. The defining property of a ratio scale, and the entire reason the third abstraction exists, is that two values are not merely located on the scale, but related to a shared, physically meaningful origin. That shared origin is what makes their sum, product, and ratio physically meaningful. A point on an arbitrary origin (the floor of a building, the GPS epoch, the freezing point of water) carries no such meaning.

5.4 Scalar and vector quantities

Absolute quantities are always scalar: they are ratio-scale magnitudes anchored at a physically meaningful true zero, and the concept of direction, which would require an orientation relative to a reference frame and not just a reference zero, has no place in the model. Vector quantities live in a vector space. They have a mathematical zero (the zero vector, representing no displacement), but not a ratio-scale physical anchor. They represent displacements from a reference and therefore always fall in the delta abstraction. Points carry a concrete origin and may be scalar or vector depending on the space they inhabit.

Abstraction
Scalar
Vector
Point point<time>, point<altitude> point<position_vector>, point<velocity>
Delta delta<duration>, delta<height> displacement, velocity (always deltas)
Absolute duration, height, mass (none, magnitude() gives a scalar absolute)

To obtain a scalar absolute from a vector quantity, take its magnitude: magnitude(velocity) → speed.

The quantity specification decides first. A specification of vector or tensor character, such as isq::velocity, has no absolute, so its plain reference is already a delta reference, and delta<isq::velocity[m / s]> names that same reference, as point<deg_C, si::ice_point> names point<deg_C>. A vector quantity needs no delta either as a value or as a type: isq::velocity(5. * m / s) is a quantity<isq::velocity[m / s], double>, a velocity along one axis, and vec * isq::velocity[m / s] is a quantity<isq::velocity[m / s], vec3>, and both are deltas. A bare unit such as m or m / s names a quantity kind, and its plain reference is an absolute reference whatever the representation, so a vector value can’t be stored under it: vec * (m / s) and quantity{vec, m / s} are ill-formed. A result computed from a vector is a delta: (2. * m) * vec scales an absolute by a vector number, and a vector is never an absolute. A vector of a kind is spelled delta<m / s>(vec), or the quantity is named, as in vec * isq::velocity[m / s]. The storage type doesn’t change the abstraction of a quantity, as it already doesn’t for a named specification. A point reference stays a point (point<isq::position_vector[m]>(vec) is a point), and a vector point needs a specification of vector character, so point<m>(vec) is ill-formed, as vec * m is. The scalar field, real or complex, plays no role: a complex scalar (an impedance, a complex power) has a true zero and is an absolute, and modulus() of it is specified as a real absolute (not yet in the prototype). A vector delta has no .absolute(), and its magnitude() is an absolute scalar. A component vector of a vector delta is a delta along an axis, even though its representation is a scalar.

5.5 Conversions between abstractions

From\To
Point
Absolute
Delta
Point Identity;
point_for(origin) to change representation
.absolute() (if not offset unit and the origin is natural_point_origin);
otherwise (point - origin_or_qp).absolute()
point - origin_or_qp;
delta_from(origin_or_qp)
Absolute Explicit ctor
using natural_point_origin
Identity Implicit construction;
.delta()
Delta origin + delta → point .absolute();
abs(), magnitude(), or modulus()
Identity

[ Note: The absolute → delta implicit conversion is a deliberate design choice. It makes common assignment and initialization convenient (quantity<delta<m>> x = 40 * m; works without any explicit cast) and mirrors how absolute magnitudes are routinely used as signed differences in practice (e.g. passing a mass to a function that takes a mass change). The consequence is that code can pass an absolute where a delta is expected without any explicit marker at the call site, which is one of the sources of the residual argument-swapping risk shown in Mass balance revisited. SG6 may wish to consider whether this conversion should require an explicit .delta() call or explicit construction (analogous to the explicit constructor used in the point case). — end note ]

The following examples demonstrate the explicit and implicit casts between all three abstractions:

// ── From Absolute ──────────────────────────────────────────────────────────────────────
quantity<isq::mass[kg]> total = 5 * kg;

// Absolute → Delta: implicit (no notation at call site) or explicit .delta()
quantity<delta<isq::mass[kg]>> d1 = total;         // implicit
quantity<delta<isq::mass[kg]>> d2 = total.delta(); // explicit

// Absolute → Point: explicit construction only (attaches natural_point_origin)
quantity<point<isq::mass[kg]>> p{total};           // explicit ctor

// ── From Delta ─────────────────────────────────────────────────────────────────────────
quantity<delta<isq::mass[kg]>> change = 3 * kg;

// Delta → Absolute: explicit .absolute(), or the magnitude
quantity<isq::mass[kg]> amount = change.absolute();
quantity<isq::mass[kg]> size   = abs(change);      // |change|

// Delta → Point: origin + delta
quantity<point<isq::length[m]>> pos = natural_point_origin<isq::length> + delta<isq::length[m]>(3);

// ── From Point ─────────────────────────────────────────────────────────────────────────
quantity<point<isq::thermodynamic_temperature[K]>> boiling{373.15 * K};

// Point → Delta: subtract a reference origin or another point
quantity<delta<isq::thermodynamic_temperature[K]>> above_zero =
    boiling.delta_from(natural_point_origin<isq::thermodynamic_temperature>);

// Point → Absolute: only valid when the point's origin is natural_point_origin
quantity<isq::thermodynamic_temperature[K]> t_abs = boiling.absolute();

An absolute may be negative in this paper, so .absolute() doesn’t check the value, and a sign check for non-negative quantities is left to [P4411R0]. The conversion is still explicit. A delta is a change, and an absolute is an amount measured from the true zero of its scale. Using one as the other is the mistake of The temperature trap: a temperature change of 28 K passed where a temperature of 28 K is needed. An implicit conversion would let that mistake compile, so .absolute() marks the place where a change is used as an amount. The opposite direction is implicit, because an amount is also its own change from zero.

5.6 Concepts

Four concepts tell what a quantity is. absolute_quantity, delta_quantity, and point_quantity each match one abstraction, and linear_quantity matches an absolute or a delta. An absolute and a delta share every operation that doesn’t look at the anchor (scaling, powers, conversions, text output), and generic code that only needs “not a point” uses linear_quantity. These concepts are what overloads and the rules of the model dispatch on.

quantity_of<Q, V> tells what a quantity can be used as. It accepts what a parameter of the corresponding concrete type accepts, so it follows the conversions between abstractions:

V
quantity_of<Q, V> matches
as a parameter of type
QS absolutes of QS quantity<QS[U]>
delta<QS> deltas and absolutes of QS quantity<delta<QS[U]>>
point<QS> points of QS quantity<point<QS[U]>>

A function that needs an amount, such as E=kTE = kT with a thermodynamic temperature, constrains its parameter with quantity_of<Q, isq::thermodynamic_temperature>, and a temperature difference is rejected, as it is by a concrete quantity<isq::thermodynamic_temperature[K]> parameter. A function that needs a change, such as an average speed from a distance traveled and an elapsed time, uses quantity_of<Q, delta<isq::length>>, and an absolute is accepted, as it would convert to a concrete delta parameter. A vector quantity is always a delta, so quantity_of<Q, isq::velocity> keeps matching it.

delta_quantity<Q> asks whether Q is a delta, and quantity_of<Q, delta<QS>> whether it can be used as one. A template that wants a change and should accept an absolute uses the latter, or linear_quantity when the specification doesn’t matter.

quantity_of<Q, delta<QS>> is defined as the absolute match of QS or the delta match of QS, so an overload constrained with quantity_of<Q, QS> is more specialized than one constrained with quantity_of<Q, delta<QS>>, and an absolute argument selects it.

5.7 Arithmetic semantics

The abstraction rules of addition, subtraction, and scaling by a number follow one principle, the zero-anchor principle. An operation preserves the absolute abstraction whenever the result remains anchored at the same physically meaningful true zero as the operands. The full rationale, including why Absolute ± Delta → Absolute was chosen and how it resolves the consistency concern between Absolute ± Delta and Absolute × Scalar, is given in Design rationale: the zero-anchor principle. Products and quotients of two absolutes are absolutes, because absolutes live on a ratio scale (see The zero-anchor principle). The rule for products and quotients with a delta operand is described in Products and quotients of two deltas.

The following tables specify the result types of arithmetic operations between the three abstractions.

5.7.1 Addition

Lhs \ Rhs
Point
Absolute
Delta
Point ✗ Point Point
Absolute Point Absolute Absolute
Delta Point Absolute Delta

Key rules:

quantity tank   = isq::mass(500 * kg);        // quantity<isq::mass[kg]>
quantity refuel = delta<isq::mass[kg]>(200);  // quantity<delta<isq::mass[kg]>>: +200 kg (signed change)
quantity loaded = tank + refuel;              // quantity<isq::mass[kg]>: 700 kg
tank += refuel;                               // same: tank is now 700 kg

// To get a signed result instead, explicitly demote before the operation:
quantity balance = tank.delta() + refuel;     // quantity<delta<isq::mass[kg]>>

5.7.2 Subtraction

Lhs \ Rhs
Point
Absolute
Delta
Point Delta Point Point
Absolute ✗ Delta Absolute
Delta ✗ Delta Delta

Key rules:

quantity before = isq::mass(100 * kg);  // quantity<isq::mass[kg]>
quantity after  = isq::mass(80 * kg);   // quantity<isq::mass[kg]>
quantity lost   = before - after;       // quantity<delta<isq::mass[kg]>>: +20 kg (a change)

quantity usage     = delta<isq::mass[kg]>(20); // quantity<delta<isq::mass[kg]>>: a signed change
quantity remaining = before - usage;           // quantity<isq::mass[kg]>: 80 kg
before -= usage;                               // same: before is now 80 kg

// To get a signed balance, demote first:
quantity deficit = before.delta() - usage;     // quantity<delta<isq::mass[kg]>>

If an absolute result is needed from an absolute − absolute subtraction (e.g., when you know the difference is non-negative), use an explicit conversion:

quantity transferred = (before - after).absolute();  // quantity<isq::mass[kg]>

5.7.3 Multiplication and division

Operation
Result
Physical Meaning
Absolute × Absolute Absolute A product of two absolute quantities (energy = power × time)
Absolute × Scalar Absolute Rescaling by a dimensionless factor (2 × mass stays absolute mass)
Absolute × Delta Delta Absolute scaled by a change is a change (e.g., area × Δheight → Δvolume)
Delta × Absolute Delta (same as Absolute × Delta; multiplication is commutative)
Delta × Scalar Delta Rescaling a signed difference (factor preserves delta abstraction)
Absolute / Absolute Absolute A physical ratio (efficiency, density)
Absolute / Delta Delta Rate of an absolute w.r.t. a signed step
Delta / Absolute Delta A change scaled by a fixed absolute reference
Delta × Delta Delta A product of two changes (see Products and quotients of two deltas)
Delta / Delta Delta A rate of change (velocity = displacement / duration)
abs(delta_real_scalar) Absolute Magnitude of a scalar delta
pow<N>(delta_scalar) Absolute An even integral N: a power of one value can’t be negative
modulus(complex_scalar) Absolute Modulus of a complex number
magnitude(delta_vector) Absolute Magnitude of a vector delta (speed = magnitude(velocity))
a % b, fmod(a, b) Dividend The abstraction of the dividend, whose sign the remainder has
remainder(a, b) Delta The remainder can be negative for any operands
hypot(x, y) Absolute Magnitude of a vector given by its components, as for abs

“Scalar” in this table means a raw C++ numeric type (double, int, etc.) treated as a multiplicative factor without an absolute/delta annotation. The table treats it as neutral: Absolute × Scalar → Absolute, whatever the sign of the scalar.

5.7.3.1 Products and quotients of two deltas

A single delta in a product is a delta by linearity: for a fixed absolute k, k * Δy is the change of k * y, so an impulse and the work of a force are changes. A product or a quotient of two deltas is not the change of anything. Δx * Δy is not the change of a product, and Δy / Δx is a slope, measured from its own zero. Whether the two deltas are correlated decides only the sign of the result, not its abstraction. The rule has to hold for binary operators, though, and “a delta only with exactly one delta factor in the numerator” doesn’t, as (Δa / Δc) * Δb would be a delta and (Δa * Δb) / Δc an absolute. Three alternative rules work for every grouping of the operands:

This paper proposes the first rule, which is the one the table above uses. In the examples of [mp-units], it costs three .absolute() calls, each after a quotient of two deltas: a glide distance computed from two altitude differences, and the durations of an aircraft tow and of a climb in a thermal, each a height change divided by a rate of climb.

A power sees one value. For a real value, pow<N>(Δx) with an even integral N can’t be negative, so it is an absolute, as abs(Δx) and hypot(Δx, Δy) are. This keeps sqrt(pow<2>(Δx) + pow<2>(Δy)) and hypot(Δx, Δy) the same type. Δx * Δx stays a delta, because a product doesn’t record that both operands are one value. Odd powers stay deltas, and so does a power of a vector.

In the Kalman filter examples of [mp-units], the rule makes a variance, the square of a standard deviation, an absolute, so the Kalman gain, a ratio of two variances, needs no .absolute(). Before the rule it needed one, as a quotient of two deltas. The call moves to the covariance update (1−K)P(1 - K) P: 1 - K is the difference of two absolutes, and so a delta, while the gain is never above one, and .absolute() checks that the updated variance isn’t negative.

5.7.4 Sign of an absolute quantity

An absolute quantity is anchored at a true zero, and nothing in this paper constrains its sign. An electric current is positive or negative relative to the reference direction chosen for it, and under the load sign convention a reactive power is positive for an inductive load and negative for a capacitive one, while both have a physically meaningful zero. If [P4411R0] is adopted, absolute quantities are where its non_negative tag is checked on values, and [P4411R0] describes where the checks run and why they can’t be elided further.

With [P4411R0], the abstraction decides which values its check applies to. A delta is never checked, as a change can have either sign. An absolute is checked when its quantity specification is non-negative. A quantity kind kind_of<QS> is defined only for a root QS and takes the domain of that root, so kind_of<isq::length>, kind_of<isq::mass>, and kind_of<isq::thermodynamic_temperature> are non-negative, while kind_of<isq::electric_current> isn’t. A value written with a bare unit is an absolute of a kind, so -5. * m and -5. * K are contract violations, and a signed value is spelled delta<m>(-5.) or delta<K>(-5.). An unnamed specification, such as isq::mass / isq::volume, is never non-negative. A point is checked when its own specification is non-negative and the chain of its origins ends at a natural origin, so point<deg_C>(-300.) is a violation through ice_point, and a point measured from an independent_point_origin, such as an epoch or a tare, never is. A possibly_negative specification under a non-negative parent, such as effective mass under mass, keeps the units and the natural origin of its parent and is never checked.

5.7.5 For discussion: a negative value written with a bare unit

This consequence of the model is the one users will hit first, and SG6 is asked to discuss it explicitly. With absolute quantities and [P4411R0] adopted, a value written with a bare unit is a checked absolute of its kind. Code that writes -5. * K to mean “5 K colder” is a contract violation, and the change has to be spelled as a delta:

quantity t1 = -5. * K;           // ✗ contract violation: a temperature 5 K below absolute zero
quantity t2 = delta<K>(-5.);     // ✓ a temperature change of -5 K

quantity o1 = -3. * m;           // ✗ contract violation: a negative length
quantity o2 = delta<m>(-3.);     // ✓ an offset of -3 m

quantity e1 = -1. * s;           // ✗ contract violation: a negative duration
quantity e2 = delta<s>(-1.);     // ✓ one second earlier

The rule is consistent with the rest of the model: 28 * K is a temperature, so -5. * K is a temperature below absolute zero, and a value doesn’t change its abstraction with its sign. The cost is that the shortest spelling of a signed change in a unit of a non-negative kind is now wrong. In a constant expression evaluated under a checking contract semantic, such as constexpr quantity t = -5. * K;, the violation fails to compile. At run time it is a contract violation only when the check runs, and under a semantic that doesn’t check, the negative absolute is stored silently. The prototype implements this check: with gsl-lite as its contract backend, constexpr quantity t = -5. * K; doesn’t compile, and -5. * K, -1 * kg, and -1. * s evaluated at run time terminate. Values of a kind that admits negative values, such as -5. * A, are unaffected.

The order of evaluation makes the rule reach further than these three lines suggest. [P3045R10] extended the multiply syntax from number * unit to quantity * unit and quantity / unit (see its chapter “Multiply syntax commutativity”), so 1 * m / s is two operations, and its intermediate result 1 * m is a quantity of its own. With checked absolutes, that intermediate can fail where the whole expression wouldn’t, and the same happens before a conversion to a signed quantity:

quantity v1 = -1. * m / s;               // ✗ contract violation: (-1. * m) is a negative length
quantity v2 = -1. * (m / s);             // unchecked, as an expression's kind is never non-negative,
                                         //   but still a negative absolute
quantity v3 = delta<m / s>(-1.);         // ✓ a signed velocity component is a change

quantity a1 = isq::altitude(-5. * m);    // ✗ contract violation: -5. * m is a negative length
quantity a2 = isq::altitude[m](-5.);     // ✓ altitude is signed
quantity a3 = -5. * isq::altitude[m];    // ✓

Under the model, v1 and v2 are both wrong spellings of a signed value, and v2 passes only because an expression carries no sign. The parentheses decide whether the mistake is caught. SG6 is asked to choose between:

The author proposes the first option.

5.7.6 Unary negation

The negation of an amount is the change that cancels it, so -mass is never a mass, while the negation of an electric current is a current flowing the other way. Which of the two applies depends on whether the quantity can be negative, which is what the non_negative tag of [P4411R0] states. The rule for -x therefore depends on whether that paper is adopted.

If [P4411R0] is adopted, -x keeps the absolute abstraction when the quantity specification of x, named or a quantity kind, admits negative values. It yields a delta for a non_negative specification, a kind whose root is non_negative, and an unnamed derived specification, whose sign the library doesn’t infer from a quantity equation. A kind takes the domain of its root, so -(5. * A) keeps the absolute abstraction and -(2. * kg) doesn’t:

quantity<isq::electric_current[A]> I1 = ..., I2 = ...;
quantity<isq::electric_current[A]> I3 = -(I1 + I2);  // an absolute current, as Kirchhoff's law needs

quantity<isq::kinetic_energy[J]> Ek = ...;
quantity<isq::thermodynamic_temperature[K]> T = ...;
auto ratio = -Ek / T;    // delta<kinetic_energy> / absolute<temperature> = delta<...>
auto Ek2 = abs(-Ek);     // the magnitude of a delta is an absolute again

The cost is that the result type of -x depends on the quantity specification, so generic code that negates an absolute has to allow for both outcomes. Algebraically equal spellings can differ as well: -I1 - I2 subtracts an absolute from an absolute and is a delta (see Algebraic-equivalence asymmetry). The abstraction already depends on the specification elsewhere, as a vector specification is never an absolute, and generic code that needs a delta writes -x.delta().

Without [P4411R0], no specification has the tag, and the rule above would keep the absolute abstraction for every named specification and kind, -mass included, so -x is a delta for every absolute instead. The result type follows from the abstraction of the operand alone, and the cost falls on quantities whose absolutes can be negative, such as electric current, electric charge, or reactive power: in I3 = -(I1 + I2) the right-hand side is a delta, and storing it in a current needs .absolute().

The prototype implements the rule for an adopted [P4411R0], with its non_negative tags. It first made -x a delta for every absolute. Switching changed no test result and no example output, and static tests were added for negations that give an absolute and for negations that give a delta.

Under either rule, -1 * x scales an absolute by a number and stays an absolute, so it can differ in type from -x. If [P4411R0] is adopted, its sign check rejects -1 * isq::mass[kg] and -1 * kg, each a negative amount, while -(1 * isq::mass[kg]) and -(1 * kg) are deltas.

5.8 Design rationale: the zero-anchor principle

The rule Absolute ± Delta → Absolute is the most visible choice in the arithmetic tables. The initial design of the library proposed the opposite rule, Absolute ± Delta → Delta, on the grounds that any such operation can produce a negative result, so the type should conservatively demote to the unrestricted delta space. This section explains why we changed it: the initial reasoning conflates two orthogonal concerns, the abstraction of a quantity and the sign constraint on its value.

5.8.1 The zero-anchor principle

The result abstraction of absolute ± delta follows from the zero-anchor principle: an operation preserves the absolute abstraction when the result remains anchored at the same physically meaningful true zero. A delta is a signed change with no inherent zero anchor. Adding or subtracting a delta from a zero-anchored quantity does not destroy the zero anchor, as the result is still measured from the same origin:

This is the rule used for points in affine spaces: point ± delta → point. Both rules express “a translation preserves the abstraction relative to its origin”. For points the origin is the affine origin; for absolutes it is the natural true zero. Unlike points, however, absolutes live on a ratio scale and support multiplication, division, and addition between two absolutes, operations that are ill-formed for points. The abstraction of the operands (absolute vs. delta) is what drives the result type, not the affine origin.

5.8.1.1 Rejected alternative: universal demotion to delta

The initial design demoted all absolute ± delta to delta, reasoning that since a delta can be negative, the result “might” be negative and the type should conservatively reflect that. This was rejected for three reasons:

  1. Type demotion is the wrong mechanism for value-range violations. The result of abs ± delta may be negative at runtime, but so may abs × scalar: a negative scalar drives the result below zero, yet Absolute × Scalar → Absolute was never in doubt. Whether a value is in range is a property of the value, and a check on the value answers it, such as the one [P4411R0] proposes. A type demotion doesn’t.

  2. It conflates abstraction with value constraint. Possibly-negative absolutes like reactive_power belong on a ratio scale regardless of their sign. Under universal demotion, reactive_power + delta<reactive_power> would yield delta<reactive_power>, changing the abstraction and making it impossible to store the result back into a reactive_power variable without an explicit conversion.

  3. It penalizes strongly-typed operands over untyped scalars. A raw C++ scalar can be negative at runtime, yet Absolute × Scalar → Absolute was never questioned. A delta is a stricter, more informative type than a raw scalar, yet the rejected rule would demote the result further than the weakly-typed case, with no principled justification.

5.8.2 Consistency with Absolute × Scalar → Absolute

Reason 3 above points to a structural requirement: the rules for Absolute × Scalar and Absolute ± Delta must be consistent because both operations are “neutral on the zero-anchor”: neither one moves the origin. Under the zero-anchor principle both preserve the absolute abstraction:

// Both rules are consistent under the zero-anchor principle:
quantity m1 = isq::mass(5 * kg);           // absolute
quantity<isq::mass[kg]> m2 = 2.0 * m1;     // absolute × scalar → absolute: 10 kg
quantity d = delta<isq::mass[kg]>(3);      // delta
quantity<isq::mass[kg]> m3 = m1 + d;       // absolute ± delta → absolute: 8 kg ✓
// Both multiply-by-scalar and add-delta are "neutral on the zero-anchor" → consistent result type

5.8.3 Handling underflow: when delta > absolute

Under the adopted rule, abs - delta keeps the absolute type, whatever the value. Consider a fuel burn where the maneuver needs more fuel than the tank holds, with the burn modeled as a delta:

quantity fuel        = isq::mass(500. * kg);        // absolute: fuel present in the tank
quantity consumption = delta<isq::mass[kg]>(600.);  // delta: how much the maneuver burns
fuel -= consumption;                                // absolute: −100 kg

This paper accepts the −100 kg absolute. If [P4411R0] is adopted, its sign check rejects it, as the prototype does.

The model is the problem. A delta is a change of something, and the change this maneuver makes to the tank is −600 kg, not +600 kg. The mass the maneuver burns is an amount: it is measured from zero and can’t be negative, so it is an absolute, and the change to the tank is -consumption. Modeled this way, abs − abs → delta gives the signed balance, and code that has to react to a deficit turns it into an amount only after checking its sign:

quantity fuel        = isq::mass(500. * kg);  // absolute: fuel present in the tank
quantity consumption = isq::mass(600. * kg);  // absolute: mass the maneuver burns
quantity balance     = fuel - consumption;    // abs − abs → delta<mass>: −100 kg
if (balance < 0 * kg) {
  quantity shortfall = abs(balance);          // absolute: 100 kg
  // handle shortfall: reduce maneuver, add fuel, abort, …
} else {
  fuel = balance.absolute();                  // the balance is not negative here
}

Code that has to keep consumption a delta gets the same signed balance by demoting first, fuel.delta() - consumption.

5.8.4 Algebraic-equivalence asymmetry

An objection raised in private review (Yongwei Wu) observes that the algebraic identity

a+b=c⇔a=c−ba + b = c \;\Longleftrightarrow\; a = c - b

does not preserve types. If a, b, c are all absolutes, the left-hand side is absolute + absolute → absolute, but the right-hand side c - b is absolute − absolute → delta. The rewritten form a = c - b then assigns a delta to an absolute, which is a type error.

This asymmetry is intrinsic to the three-abstraction model. The rule absolute − absolute → delta reflects a physical fact: the difference between two zero-anchored magnitudes is a signed change, not a new magnitude. For a quantity whose values can’t be negative this is especially clear (a convex cone is closed under addition but not under subtraction by definition), but the asymmetry holds for all absolutes. Even electric_current − electric_current → delta<electric_current>: the difference between two current values is a signed change, not a current level. Removing the asymmetry would require abs − abs → abs, which labels a difference as an amount and so would abandon the distinction between absolute and delta.

// The identity a + b = c does NOT mean c − b has the same type as a:
quantity<isq::mass[kg]> a = 30 * kg;        // absolute
quantity<isq::mass[kg]> b = 70 * kg;        // absolute
quantity<isq::mass[kg]> c = a + b;          // absolute: 100 kg

// quantity<isq::mass[kg]>     a2 = c - b;             // ✗ delta<mass> ≠ absolute<mass> (type error)
quantity<delta<isq::mass[kg]>> diff = c - b;           // delta<mass>: 30 kg — correct delta type
quantity<isq::mass[kg]>        a2   = diff.absolute(); // explicit re-entry: 30 kg

5.9 Text output

An absolute quantity prints like any quantity: 42 * kg prints 42 kg, the same text as delta<kg>(42), so absolutes need no formatter of their own. The output of points is discussed in [P3045R10].

5.10 Temperature revisited

With absolute quantities, the temperature use case from The temperature trap is resolved. The key change is in what an offset unit is and in how absolute_zero is defined.

In [P3045R10], absolute_zero is an absolute_point_origin, and kelvin declares it as its point origin, which triggers the multiply syntax restriction and prevents 28 * K from compiling. Under the absolute quantities model, an offset unit is a unit whose point origin is not a natural origin, that is, doesn’t compare equal to natural_point_origin<QS>. The natural origin of a kind marks the same true zero as the natural origin of a quantity of that kind, so natural_point_origin<isq::thermodynamic_temperature> and natural_point_origin<kind_of<isq::thermodynamic_temperature>> compare equal. si::absolute_zero is the natural origin of thermodynamic temperature under a name of its own, and kelvin keeps naming it as its origin, so the kelvin is not an offset unit. The name exists so that the temp - absolute_zero idiom of The temperature trap keeps working and so that types print a meaningful origin. ice_point stays a relative origin, an offset in millikelvin from the kelvin scale, so degree_Celsius is an offset unit, and so is degree_Fahrenheit. In the prototype, natural_point_origin_ is the class type of natural_point_origin:

inline constexpr struct absolute_zero : natural_point_origin_<kind_of<isq::thermodynamic_temperature>> {} absolute_zero;
inline constexpr struct kelvin : named_unit<"K", kind_of<isq::thermodynamic_temperature>, absolute_zero> {} kelvin;

// Celsius chain: offset value 273.150 K unchanged
inline constexpr struct ice_point : relative_point_origin<point<milli<kelvin>>(273'150)> {} ice_point;
inline constexpr struct degree_Celsius : named_unit<symbol_text{u8"°C", "`C"}, kelvin, ice_point> {} degree_Celsius;

With this definition, 28 * K now creates an absolute thermodynamic temperature directly, with no conversion idiom required:

quantity T = 28. * K;                    // absolute temperature — compiles in new design

The ideal gas law can be written directly with absolute temperatures:

quantity P = 1. * atm;
quantity V = 1. * L;
quantity n = 1. * mol;
quantity T = 301.15 * K;                 // absolute temperature
quantity R_calc = P * V / (n * T);       // compiles, correct

When working with Celsius input, the conversion path is explicit but straightforward:

// auto T = 28. * deg_C;                 // still does not compile — offset unit
quantity temp = point<deg_C>(28.);       // 28 °C on the Celsius scale
// quantity T = temp.absolute();         // does not compile: the origin is ice_point, not a natural one
quantity T = temp.in(K).absolute();      // 301.15 K — absolute temperature
quantity R_calc = P * V / (n * T);       // compiles, correct

The .in(K).absolute() chain is a type-safety checkpoint: it forces the programmer to acknowledge the shift from an interval-scale point (location on the Celsius scale) to a ratio-scale magnitude (thermodynamic temperature measured from absolute zero). This replaces the [P3045R10] workaround temp.in(K).delta_from_zero() with a more direct name that reflects the mathematical intent.

Offset units (°C, °F) are never absolute, because their origin is not a natural origin. A point on the Celsius scale is not an absolute temperature, and it must be converted explicitly. A point in kelvins is measured from absolute_zero, a natural origin, so point<K>(28.).absolute() is 28. * K. .in(U) changes only the unit of a point, never its quantity specification, so point<isq::thermodynamic_temperature[deg_C]>(20.).in(K) is still a point of isq::thermodynamic_temperature.

The lower bound of a Celsius point needs no bounds policy on ice_point. If [P4411R0] is adopted, a point of a non-negative specification is checked when the chain of its origins ends at a natural origin, and the chain of ice_point ends at absolute_zero. So point<deg_C>(-300.) and point<deg_F>(-460) are contract violations, while point<deg_C>(-273) with an int representation isn’t: the floor is checked exactly in the common type of the value and the offset of ice_point, which an int in degrees Celsius can’t hold.

[ Note: The point abstraction in this library generalizes beyond pure affine spaces. A pure affine space is unbounded (no origin, no bounds) and is the right model for, e.g., spatial position. The Celsius scale is a convex space in the taxonomy of [Rosten2025]: it is bounded below by the 0 K boundary (see Overview) but has no ratio-scale physical anchor of its own. The point type covers both: unbounded interval scales (affine spaces) and bounded scales (convex spaces). What they share is the absence of a ratio-scale anchor. They are locations relative to a chosen reference, not amounts measured from true physical zero. — end note ]

This distinction has a measurement-theory interpretation. Physically, Kelvin quantities live in an absolute convex space: the domain is the half-line [0,+∞)[0, +\infty) with a physically distinguished origin (absolute zero), and addition of two Kelvin values is well-defined because both are measured from that common anchor. Celsius points, by contrast, are defined as a translation of the Kelvin scale (via ice_point), so their lower bound is a consequence of that translation, not an independent property of the Celsius origin. Adding two Celsius temperatures is ill-formed for the same reason as any affine point addition: a Celsius point is a location on the scale, not an amount measured from true zero. The .in(K).absolute() call is the operation that transports a value from the convex-space (Celsius) domain into the absolute convex (Kelvin) domain by making the true-zero anchor explicit. The type of a Kelvin absolute doesn’t enforce the lower bound. If [P4411R0] is adopted, its sign check on the value does (see Sign of an absolute quantity).

5.11 Mass balance revisited

With absolute quantities, the mass balance use case from Mass balance and accumulation gains type-safety through the distinction between absolute and delta:

quantity<delta<percent>> moisture_loss(quantity<delta<kg>> water_lost, quantity<kg> total)
{
  return water_lost / total;
}

quantity total_initial = 100 * kg;                        // absolute mass
quantity total_dried   = 80 * kg;                         // absolute mass
quantity water_lost    = total_initial - total_dried;     // delta<mass>: +20 kg (difference)

quantity loss = moisture_loss(water_lost, total_initial); // Correct

// Attempting to swap arguments:
// auto loss = moisture_loss(total_initial, water_lost);
//   arg1: absolute → delta<kg> param: ✓ compiles — absolute→delta is implicit
//   arg2: delta<kg> → absolute param: ✗ fails — delta→absolute requires explicit .absolute()
// The call fails overall, but only because of the second argument.

// A subtler mistake — two absolutes — also compiles:
// auto loss = moisture_loss(total_initial, total_initial);
//   arg1: absolute → delta<kg> param: ✓ implicit
//   arg2: absolute → absolute param: ✓ identity
// Result: total_initial / total_initial = 1.0 (100%) — wrong answer, no diagnostic

The three-way split is a partial solution to the argument-swapping problem. It rejects passing a delta where an absolute is required, but moisture_loss(total_initial, total_initial) still compiles because the implicit absolute → delta conversion (see the conversion note) allows total_initial to satisfy the delta<kg> first parameter. This is not a defect in the split; it is a consequence of a deliberate design decision. If SG6 decides to require an explicit .delta() call for that conversion, the subtler mistake would also become a compile error. Until then, the residual argument-swap risk remains in the domain of naming conventions and code review.

5.12 Averages and centers of mass

At Brno 2026, SG6 asked how to average temperature readings in degrees Celsius and how to compute a center of mass, when two points can’t be added and a point can’t be multiplied by a mass. The textbook formula adds the values first:

using celsius_reading = quantity<point<deg_C>>;
std::array<celsius_reading, 3> readings{point<deg_C>(20.), point<deg_C>(22.5), point<deg_C>(26.)};

quantity avg = (readings[0] + readings[1] + readings[2]) / 3;  // ✗ point + point is ill-formed

The same expression compiles for absolute temperatures in kelvins, as absolutes can be added. For Celsius readings it must not compile: the sum of two readings depends on the origin of the scale. 20 °C and 22.5 °C would sum to 42.5 °C on the Celsius scale, but to 588.8 K, which is 315.65 °C, on the Kelvin scale. The mean doesn’t depend on the origin. It can be written with differences only:

x‾=x1+1n∑i=1n(xi−x1)\bar{x} = x_1 + \frac{1}{n}\sum_{i=1}^{n}(x_i - x_1)

Every operation on the right is valid for points: xi−x1x_i - x_1 is a delta, a delta divided by a number is a delta, and a point plus a delta is a point. A weighted mean has the same form,

x‾=x1+∑iwi(xi−x1)∑iwi\bar{x} = x_1 + \frac{\sum_{i} w_i (x_i - x_1)}{\sum_{i} w_i}

and with masses as weights and positions as points it is the center of mass, computed without multiplying a point by a mass. For two masses, the model already allows writing it by hand:

using vec3 = cartesian_vector<double>;  // a vector type of the reference implementation
using position = quantity<point<isq::position_vector[m]>, vec3>;

position r1 = point<isq::position_vector[m]>(vec3{0., 0., 0.});
position r2 = point<isq::position_vector[m]>(vec3{4., 0., 0.});
quantity m1 = 2. * kg;
quantity m2 = 1. * kg;

position center = r1 + (r2 - r1) * (m2 / (m1 + m2));  // [1.333, 0, 0] m

The reference implementation provides both algorithms in its utility library as mp_units::utility::mean and mp_units::utility::weighted_mean. They accept ranges of absolutes, deltas, or points, and return the type of the values:

celsius_reading avg = utility::mean(readings);  // 22.83 °C, measured from the ice point

std::array<position, 3> positions{point<isq::position_vector[m]>(vec3{0., 0., 0.}),
                                  point<isq::position_vector[m]>(vec3{4., 0., 0.}),
                                  point<isq::position_vector[m]>(vec3{0., 8., 0.})};
std::array masses{2. * kg, 1. * kg, 1. * kg};
position center = utility::weighted_mean(positions, masses);  // [1, 2, 0] m

Neither function adds the values up. Every intermediate value is a difference from the first value or from the running mean, so only the difference between the largest and the smallest value has to fit in the representation. This also avoids the overflow of the textbook formula for absolutes that could be summed directly. An integral mean accepts every integer type, unsigned ones included, and keeps the floor of the mean exactly, as a running mean and a remainder. Like std::midpoint, it takes each difference in the unsigned type of the representation, as a distance and a direction, so nothing wraps. Unlike std::midpoint, which rounds toward its first argument, it rounds down, which doesn’t depend on the order of the values. The two functions are not proposed in this paper.

5.13 Impact on [P3045R10]

This is a breaking change to the initial proposal:

  1. quantity<R> becomes an absolute quantity instead of a delta.
  2. Users who need a delta must write quantity<delta<R>>, use the delta<R>(value) construction helper, or call .delta() on an existing absolute quantity.
  3. absolute − absolute returns a delta, not an absolute (the same rule as point − point → delta). Existing code that assigns such a result to an absolute variable will fail to compile; the fix is either to change the target type to delta<R> or to call .absolute() on the result to explicitly re-enter the absolute domain.
  4. linear_quantity, the concept of an absolute or a delta, joins the new absolute_quantity and the delta_quantity and point_quantity of [P3045R10]. Generic code that constrained a parameter with “a delta” to mean “not a point” migrates to it. quantity_of<Q, QS> matches only absolutes (see Concepts). Code written with plain spellings such as 5 * m gets absolutes and keeps matching. Only a difference or an explicit delta passed where quantity_of<Q, QS> is required now needs delta<QS> in the constraint.
  5. A quantity_like_traits specialization whose reference is a bare unit now imports an absolute, so the std::chrono::duration specialization names delta<R> to stay a delta. A time_point stays a point.
  6. A quantity of a vector or tensor specification is never an absolute, so without point<> it stays a delta, and its type keeps its spelling: quantity<isq::velocity[m / s], vec3> needs no change. A vector value of a bare unit can’t be an absolute either, so vec * m and quantity<m, vec3> become ill-formed, and the value is spelled delta<m>(vec), of type quantity<delta<m>, vec3>. A vector point of a bare unit, point<m>(vec) or quantity<point<m>, vec3>, is ill-formed too, and is spelled with a vector specification, as in point<isq::position_vector[m]>(vec).
  7. A value of a bare unit is an absolute of its kind. If [P4411R0] is adopted, a negative value of a non-negative kind, such as -5. * K written for a temperature change, is a contract violation, and the change is spelled delta<K>(-5.) (see For discussion).
  8. -x on an absolute is a delta when its quantity specification doesn’t admit negative values or is an unnamed derived specification, and for every absolute if [P4411R0] isn’t adopted (see Unary negation).
  9. Compound assignment keeps the abstraction of its left operand, so a -= b with two absolutes is ill-formed.

However:

5.14 Implementation experience

Absolute quantities are implemented on a prototype branch off the version 3 line of [mp-units] (October 2026). The library, its tests, and its examples build and pass with GCC 15 and Clang 21.

The implementation went more smoothly than we expected. Most of the library changed mechanically. The tests and the examples needed changes for one reason: the previous model couldn’t express what the code meant. An amount and a change were the same type, so code written for that model had to approximate its intent. With three abstractions, each of those approximations was replaced by what the code meant, and some of them turned out to be real errors (see The model finds real errors). Of the rules of R1, only unary negation needed a second look. The prototype also filled in the cases R1 left open and raised a few questions for SG6. This chapter records what it taught us.

5.14.1 The model finds real errors

The rule absolute − absolute → delta exposed category errors in the library itself. Five functions stored the difference of two positions back into a position type: floor and ceil, wrap_to_range and reflect_in_range, and the canonical form of a spherical vector.

Together with the non_negative check of [P4411R0], the model also found signed changes stored as absolutes in the tests: negative durations fed to the rounding functions, a −3 m projection offset stored as a length, negative origin bounds, and negative and NaN inputs of formatting tests. Checking values written with a bare unit changed about 300 more lines of the tests, nearly all of them negative literals such as -42 * m used as signed numbers and now spelled delta<m>(-42), and the glide computer example, whose sink rates are written delta<m / s>(-0.7389). In the examples it found three modeling errors:

5.14.2 Rules the prototype added

R1 left these cases open.

5.14.3 What generic code needs

Most generic code in the library, and nearly every generic test helper, needs “an absolute or a delta”, which is to say “not a point”, and the first version had no name for that. This is where linear_quantity comes from. A survey of the generic code found no place that needs a delta but must reject an absolute, while about 18 places in the examples need an amount and about 12 a change that an absolute may stand for. This decided the meaning of quantity_of described in Concepts.

5.14.4 Open questions

The magnitude of a vector. A vector is never an absolute. Engineering code often stores the magnitude of a vector in a double under the vector’s specification, and then gets a delta. The kinetic energy of a rotation, moment_of_inertia * pow<2>(angular_velocity), and the power torque * angular_velocity are deltas under the proposed rule for Products and quotients of two deltas, and would be absolutes if two deltas cancelled and a power followed the same rule. The storage tank example divides by an isq::weight, which is a force and so a vector, and its fill level is a delta under every rule. ISQ names no scalar specification for the magnitude of a weight. Where the code means the magnitude, magnitude() of the vector gives a scalar absolute.

The name of absolute_point_origin. The word “absolute” names an abstraction of quantities, and it also names the origin that starts an independent affine space, absolute_point_origin, while si::absolute_zero is a natural origin. The reference implementation renames the origin and keeps the old name, deprecated. The candidates are:

root_point_origin and base_point_origin aren’t candidates, because “root” names the root of a quantity kind and “base” names base quantities and units. This paper proposes independent_point_origin, and its examples keep the current spelling until SG6 chooses a name.

A negative value written with a bare unit. If [P4411R0] is adopted, -5. * K written for a temperature change is a contract violation, and the change is spelled delta<K>(-5.). This is the consequence users will meet first, and it is discussed in For discussion and polled below.

Compile time is not measured yet. An A/B against the branch base precedes any merge.

The model adds three abstractions, four concepts, the delta<QS> and point<QS> selectors, and rules for compound assignment. A user needs three sentences: a plain quantity is measured from zero, a subtraction gives a change, and a reading on a scale with its own origin is a point.

6 Summary

Absolute quantities add a third abstraction, anchored at a true zero, to the delta and the point of [P3045R10]. They make 28 * K an absolute temperature that the ideal gas law accepts, and they let a function signature tell an amount from a change. They change the default meaning of quantity<R>, so they have to be decided before the initial API of [P3045R10] is locked.

Affine relationships within the ISQ hierarchy (such as position vector and displacement) are planned for a later revision of [P3045R10], and runtime frame projections as a paper of their own.

7 Proposed polls

7.1 SG6

  1. SG6 supports adding absolute quantities, a third abstraction of the quantity class template next to deltas and points, as proposed in P4185R2.
  2. quantity<R> should denote an absolute quantity, and a delta should be spelled quantity<delta<R>>.
  3. An absolute quantity should convert to a delta implicitly, as proposed in P4185R2.
  4. A product or a quotient with a delta operand should be a delta, as proposed in P4185R2.
  5. For an absolute x, -x should stay an absolute when the quantity specification of x, named or a quantity kind, admits negative values according to [P4411R0], and be a delta otherwise. If [P4411R0] isn’t adopted, -x should be a delta for every absolute, as proposed in Unary negation.
  6. quantity_of<Q, QS> should match the absolutes of QS, and quantity_of<Q, delta<QS>> its deltas and absolutes, as proposed in Concepts.
  7. absolute_point_origin should be renamed to independent_point_origin.
  8. If [P4411R0] is adopted, a value written with a bare unit of a non-negative quantity kind, such as -5. * K, should be an absolute and be checked, and a signed change should be spelled delta<K>(-5.) (see For discussion).

8 Next steps

If SG6 gives a favorable direction on absolute quantities, the path forward is:

  1. Finish the implementation in the [mp-units] reference implementation and measure its compile time. Absolute quantities are implemented on a prototype branch off its version 3 line (see Implementation experience).

  2. Validate against real-world use cases. The existing [mp-units] user base will exercise the new abstraction against production physics, embedded systems, and financial code. Design issues should surface at this stage, before standardization.

  3. Merge absolute quantities into P3045. They are designed as a breaking change within [P3045R10], to be merged into it rather than proposed on their own. A future revision of P3045 will incorporate them and remove the design alternatives not chosen by SG6.

  4. Submit the merged paper to LEWG for design review and approval.

9 Acknowledgments

Special thanks and recognition goes to The C++ Alliance for supporting Mateusz’s membership in the ISO C++ Committee and the production of this proposal.

The author also thanks Oliver Rosten for independently developing the convex-space model [Rosten2025] that contributed the four-space taxonomy and several key design insights used throughout this paper; Tiago Freire for the ideal gas law demonstration that motivated the temperature trap analysis and for detailed ANSI review; Chip Hogg for suggesting the nature-based constant idiom; Yongwei Wu for raising the algebraic identity objection against absolute subtraction; and Amir Kirsh for review feedback.

10 References

[mp-units] mp-units - A Physical Quantities and Units library for C++.
https://mpusz.github.io/mp-units
[P3045R10] Mateusz Pusz. Quantities and units library.
https://wg21.link/p3045r10
[P3045R7] Mateusz Pusz, Dominik Berner, Johel Ernesto Guerrero Peña, Charles Hogg, Nicolas Holthaus, Roth Michaels, Vincent Reverdy. 2026-02-23. Quantities and units library.
https://wg21.link/p3045r7
[P4185R0] Mateusz Pusz. 2026-05-11. Completing the Mathematical Model for C++ Quantities and Units.
https://wg21.link/p4185r0
[P4185R1] Mateusz Pusz. 2026-06-12. Completing the Mathematical Model for C++ Quantities and Units.
https://wg21.link/p4185r1
[P4406R0] Mateusz Pusz. Comparing Quantities Against Zero.
https://wg21.link/p4406r0
[P4409R0] Mateusz Pusz. Integer Division in a Quantities Library.
https://wg21.link/p4409r0
[P4411R0] Mateusz Pusz. Non-Negative Quantity Specifications.
https://wg21.link/p4411r0
[P4412R0] Mateusz Pusz. Range-Validated Quantity Points.
https://wg21.link/p4412r0
[Rosten2025] Oliver J. Rosten. Thoughts on Quantities & Units — Putting P3045 on a firm foundation.
https://docs.google.com/presentation/d/1qjw0SURoJtX_DhaFUr62F-BXtQJjdQRqzMUHvQ6h3-E/edit?usp=sharing
[Sequoia] Oliver J. Rosten. Sequoia — a C++ library exploring mathematical foundations for quantities.
https://github.com/ojrosten/sequoia