Non-Negative Quantity Specifications
Theme
Contents

Non-Negative Quantity Specifications

Non-Negative Quantity Specifications

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

1 Revision history

1.1 R0

Initial revision. The material comes from the chapter “Non-negative quantities” of [P4185R1] and from the motivation that preceded it. At Brno 2026, SG6 encouraged splitting the chapter “Range validated quantity points” of [P4185R1] into a paper of its own. This paper does the same for non-negativity, so that it can be decided separately. Changes since the chapter “Non-negative quantities” of [P4185R1]:

2 Introduction

Many quantities can’t be negative. No amount of mass, duration, or volume is below zero, and ISO 80000-3 defines speed as the magnitude of velocity. [P3045R10] has no way to express this. This is why every function that relies on it repeats a precondition, and every reviewer has to guess which parameters carry the guarantee.

This paper proposes to record the domain once, in the definition of the quantity:

All four are additions to [P3045R10], which proposes the framework and no system of quantities, so no definition in it changes meaning. The ISQ definitions of [P4213R1], in the same mailing, already use non_negative and possibly_negative; if this paper isn’t adopted, those tags are dropped. The four features are implemented in [mp-units].

Three related features are proposed with their own polls. A point of a non-negative quantity, or of a quantity kind whose root is non-negative, measured from the true zero is checked automatically (see Checked points). Inference from defining equations would let a named derived quantity such as mass density take the property from its equation. It has been prototyped but is not implemented in [mp-units]. If absolute quantities [P4185R2] are adopted, the tag is also checked on their values (see Enforcement on absolute quantities).

We ask SG6 for a direction before the ISQ definitions of [P4213R1] are reviewed, because the domain of a quantity is part of its definition.

3 Motivation

3.1 Preconditions as a symptom

Consider two functions whose inputs and results are non-negative by their physics. With the Expects macro of the Guidelines Support Library [GSL], they read:

quantity<isq::mass_density[kg / m3]> density(quantity<isq::mass[kg]> mass, quantity<isq::volume[m3]> volume)
{
  Expects(mass >= 0 * kg);
  Expects(volume > 0 * m3);
  return isq::mass_density(mass / volume);
}

quantity<isq::kinetic_energy[J]> kinetic_energy(quantity<isq::mass[kg]> mass, quantity<isq::speed[m / s]> speed)
{
  Expects(mass >= 0 * kg);
  Expects(speed >= 0 * m / s);
  return isq::kinetic_energy(mass * pow<2>(speed) / 2);
}

C++26 contracts [P2900R14] let the author say even more. The preconditions move to the declaration, where a caller can see them, and a postcondition adds one more check, on the result:

quantity<isq::mass_density[kg / m3]> density(quantity<isq::mass[kg]> mass, quantity<isq::volume[m3]> volume)
  pre(mass >= 0 * kg)
  pre(volume > 0 * m3)
  post(r: r >= 0 * kg / m3)
{
  return isq::mass_density(mass / volume);
}

Each function above repeats mass >= 0 * kg, and so will every other function in the code base that takes a mass. A mass can’t be negative whatever function it is passed to, so the condition belongs in the definition of isq::mass, but the type system gives the author no place there to state it. As a result:

The last point is the one that a convention can’t fix. A precondition w >= 0 * J written for kinetic energies rejects a valid negative work, done against a force that opposes the motion, and dropping it loses the check for kinetic energies. Only the type of the argument says which of the two it is.

This paper addresses the last point. With absolute quantities [P4185R2], it also removes the >= 0 preconditions: a quantity<isq::mass[kg]> is then an absolute, checked when it is created (see Enforcement on absolute quantities), and density keeps only pre(volume != 0 * m3), the one condition the domain doesn’t cover. With deltas and points only, a quantity<isq::mass> may hold a negative difference of two masses, so the function still has to check. What the tag gives with deltas and points describes the benefits of this paper in that case.

3.2 The domain belongs in the definition of the quantity

The ISQ decides the domain for some quantities: speed is the magnitude of velocity, and length, area, and volume measure extents. For others it is a decision that needs review (see Why the tag should come before the ISQ definitions).

[P3045R10] already records the character of a quantity in its definition. Whether a quantity is a scalar or a vector is stated once in its quantity_spec, inherited by its children, and checked by the library wherever it matters. We propose the same for the domain.

Oliver Rosten raised the missing domain for P3045 [Rosten2025], and [Sequoia] models it. A check written in user code can’t be inherited by a child quantity or queried by generic code.

4 Proposal

4.1 The non_negative tag

non_negative is a property passed to a named_quantity_spec, in the same position as subkind or a quantity_character:

inline constexpr struct length           : named_quantity_spec<dim_length, non_negative> {} length;
inline constexpr struct mass             : named_quantity_spec<dim_mass, non_negative> {} mass;
inline constexpr struct duration         : named_quantity_spec<dim_time, non_negative> {} duration;
inline constexpr struct electric_current : named_quantity_spec<dim_electric_current> {} electric_current;

Electric current carries no tag, because a current flows in either direction. Among the base quantities of the ISQ, it is the only one that [mp-units] leaves untagged.

The tag applies only to real scalar quantities. A vector, a tensor, or a complex quantity has no order on its values, so non_negative on such a specification is ill-formed:

// ill-formed: non_negative can only be applied to real scalar quantities
inline constexpr struct bad : named_quantity_spec<isq::length, quantity_tensor_order::vector, non_negative> {} bad;

is_non_negative(qs) is the corresponding query:

template<quantity_spec Q>
consteval bool is_non_negative(Q);

For a quantity kind, kind_of<QS>, the query answers for its root QS (see Inheritance through the hierarchy).

4.2 Inheritance through the hierarchy

A child of a quantity is a more specific quantity of the same kind. Every width, diameter, and radius is a length, so if no length is negative, none of them is either. A real-scalar child therefore inherits non_negative from its parent:

inline constexpr struct width  : named_quantity_spec<isq::length> {} width;
inline constexpr struct radius : named_quantity_spec<width> {} radius;

static_assert(is_non_negative(width));
static_assert(is_non_negative(radius));

Inheritance stops in two places:

A quantity kind takes the domain of its root. kind_of<QS> is defined only for the root QS of a kind tree, and a value of the kind, such as 42 * m, is an amount of that root. A signed member of the tree, such as altitude or the vector displacement, states its own domain when it is named, and a signed value of the kind is a delta. kind_of<isq::length>, which is what 42 * m carries, is therefore non-negative, and the kind of a signed root is not:

static_assert( is_non_negative(kind_of<isq::length>));
static_assert( is_non_negative(kind_of<isq::thermodynamic_temperature>));
static_assert(!is_non_negative(kind_of<isq::electric_current>));

A kind is never the parent of a named quantity, so this rule doesn’t change the domain of any named quantity.

A child of a signed parent may carry the tag on its own. Kinetic energy is the example: energy carries no tag and is signed, while 12mv2\tfrac{1}{2}mv^2 is never negative for a non-negative mass.

inline constexpr struct kinetic_energy
    : named_quantity_spec<mechanical_energy, mass * pow<2>(speed), non_negative> {} kinetic_energy;

A nested kind created with subkind inherits as any other child does. A fluid_head defined as named_quantity_spec<isq::height, subkind> is non-negative.

4.3 Leaving the parent’s domain with possibly_negative

Once a standard tags mass non_negative, inheritance makes every child of mass non-negative. A quantity that is a mass but takes negative values then has no good place. In [P3045R10] a unit belongs to a kind (the gram is defined for kind_of<isq::mass>), so only a quantity of the mass kind may use the kilogram:

possibly_negative covers the missing case. It cancels the non_negative a quantity would otherwise inherit, while the parent, and with it the kind and the units, stay the same. A correct hierarchy seldom places a signed quantity below a non-negative one, so the tag is rare. Where it is needed, nothing else lets the quantity keep the units of its family. Without it, tagging a quantity in the standard would close its subtree to signed quantities for good.

ISO 80000-12 gives one such quantity: the effective mass of a charge carrier is negative near the top of an energy band, where the band curves downward, and it is expressed in kilograms:

inline constexpr struct effective_mass : named_quantity_spec<isq::mass, possibly_negative> {} effective_mass;

static_assert(!is_non_negative(effective_mass));
quantity m1 = point<effective_mass[kg]>(-4.6e-31);  // OK: about -0.5 electron masses
quantity m2 = point<isq::mass[kg]>(-4.6e-31);       // contract violation

The two tags are mutually exclusive, and naming both is ill-formed.

Effective mass is the only quantity of the ISQ that needs possibly_negative. [mp-units] also uses the tag, for now, on altitude, a signed vertical coordinate under length whose non_negative child is height, so that a height converts implicitly to an altitude. Once an altitude is modeled as a point of height, as planned, that tag goes away.

Most uses are expected outside the standard, in libraries for physics that extends a domain. A spin system in a magnetic field reaches negative absolute temperatures [Purcell1951], and so do the motional degrees of freedom of atoms in an optical lattice [Braun2013]. A library for such systems declares one child and keeps the kelvin and its origin, si::absolute_zero. A point of a possibly_negative quantity isn’t checked, even when it is measured from the true zero (see Checked points):

inline constexpr struct spin_temperature
    : named_quantity_spec<isq::thermodynamic_temperature, possibly_negative> {} spin_temperature;

quantity t = point<spin_temperature[K]>(-0.5);  // OK: 0.5 K below si::absolute_zero

An effective_mass still converts implicitly to mass, because every effective mass is a mass. With deltas only, that conversion is not checked, and a negative value ends up in a quantity<isq::mass>. With absolute quantities, it is checked (see Where the check runs).

Here the hierarchy is not a subset relation of domains: a possibly_negative child is wider than its parent. The same is true of a real-scalar quantity below a vector node, which doesn’t regain the property. A rate of climb, declared as a child of velocity and stored in a scalar representation, is signed and is wider than its non-negative ancestor speed, to which it converts implicitly.

4.4 Non-negativity and individual values

non_negative says that every amount of the quantity, measured from its true zero, is non-negative. A difference of two amounts is a delta and may still be negative. The property is a statement about a quantity, and the result of one operation can’t establish it.

Some operations never produce a negative value: abs() (for a representation in which negation is defined, since abs(INT_MIN) is undefined), a vector norm, the modulus of a complex number, and the square of one real value. Still, the type of their result usually doesn’t say so: abs() keeps the quantity specification of its argument, which may be signed. The absolute value of a mechanical work is still a mechanical work, and work spans the whole real line, so there is no non-negative specification for abs() to return. A norm can map to a magnitude quantity, as speed for velocity. A general mapping, a magnitude(QS) operation on quantity specifications, is planned for [mp-units] and described in [mp-units quantity safety], and its result would be non-negative by construction. It covers vector quantities only. For a signed scalar such as work, the only alternative would be a second specification meaning “the non-negative part of QS” for every quantity, which would double the tree.

With absolute quantities [P4185R2], abs(), hypot(), a vector norm, and an even power of a scalar return absolutes, while the product dx * dx stays a delta. Their results are then amounts measured from zero, and the result of a non-negative specification is checked as described in Enforcement on absolute quantities.

Without absolute quantities, and for a signed specification such as work even with them, the guarantee belongs in the contract of the operation, as a postcondition. For abs():

template<auto R, typename Rep>
constexpr quantity<R, Rep> abs(const quantity<R, Rep>& q) noexcept
  post(r: !(r < quantity<R, Rep>::zero()));

Written as “not less than zero”, the postcondition also holds for a NaN, which isn’t negative (see Checked points). It documents the contract and can be checked, without a new type.

4.5 Expressions carry no flag

The property belongs to named quantity specifications. The unnamed specification of an expression, such as the isq::mass / isq::volume of mass / volume, never carries it:

static_assert( is_non_negative(isq::mass_density));
static_assert(!is_non_negative(isq::mass / isq::volume));

The reason is that a raw number has no type-level sign. -1.0 * (mass / volume) has the same type as mass / volume, and so does std::sin(phi) * mass / volume. A flag on the expression type would claim something the type can’t know. The value enters a non-negative quantity only when it is converted to a named specification, as in isq::mass_density(mass / volume).

5 What the tag gives with deltas and points

[P3045R10] has two abstractions: a delta (quantity<R, Rep>) and a point (quantity<point<R, PO>, Rep>). A delta is a difference. The difference of two masses is a signed value with the type quantity<isq::mass>, so the library can’t reject a negative delta of a non-negative quantity.

5.1 Checked points

A point is a position measured from an origin, so a point of a non-negative quantity measured from its true zero can’t lie below it. We propose that such a point be checked with no code from the user. A point is checked if and only if:

The true zero is then the floor of the point: the check is that the offset of the point’s origin from the natural origin, plus the value of the point, is not negative. It is a precondition of every operation that stores a point, so it follows the evaluation semantic in effect, during constant evaluation as well:

quantity d1 = point<isq::distance[m]>(5.0);   // OK
quantity d2 = point<isq::distance[m]>(-1.0);  // contract violation

quantity d3 = point<isq::distance[m]>(1.0);
d3 -= isq::distance(3.0 * m);                 // contract violation

quantity d4 = point<m>(-1.0);                 // contract violation: kind_of<isq::length>
quantity a1 = point<isq::altitude[m]>(-5.0);  // OK: an altitude is signed

The floor is a property of the quantity specification. It needs no bounds policy on the natural origin, and it doesn’t depend on [P4412R0], whose range policies stay properties of origins. With [P4412R0], a program can choose another response with the constraint_violation_handler of its representation, or measure from an absolute_point_origin of its own with another policy, for example clamp_non_negative to clamp the rounding noise of a floating-point computation to zero.

A relative origin defined from the natural origin keeps the floor. Points measured from it may be negative as long as they stay above the true zero:

inline constexpr struct average_height : relative_point_origin<natural_point_origin<isq::height> + 1.7 * m> {} average_height;

quantity h1 = average_height - 0.5 * m;  // OK: 1.2 m above the floor
quantity h2 = average_height - 2.0 * m;  // contract violation: 0.3 m below the floor

The quantity of the origin plays no role. A height measured from relative_point_origin<point<isq::altitude[m]>(100.)> is floored at the true zero, because height is non-negative and the chain ends at a natural origin, so a height of -200 m from that origin is a contract violation, while an altitude of -200 m from it isn’t.

The origin of the kelvin, si::absolute_zero, is the natural origin of thermodynamic temperature under a name of its own. It compares equal to natural_point_origin<isq::thermodynamic_temperature>, and the name lets a program write t - si::absolute_zero and makes the types print a meaningful name. The kelvin is therefore not an offset unit. The degree Celsius, and in [mp-units] the degree Fahrenheit, are offset units, measured from the relative origins si::ice_point and usc::fahrenheit_zero, whose chain ends at si::absolute_zero. Points in all three units are checked:

quantity t1 = point<K>(-1.0);                                        // contract violation
quantity t2 = point<isq::thermodynamic_temperature[deg_C]>(-300.0);  // contract violation
quantity t3 = point<deg_F>(-460);                                    // contract violation: below -459.67 °F
quantity t4 = point<deg_C>(-273);                                    // OK: 0.15 K above absolute zero
quantity t5 = point<deg_C>(-274);                                    // contract violation

The check is exact. It runs in the common type of the value and the offset to the natural origin, so the offset doesn’t have to be representable in the point’s own type: t4 and t5 store an int in degrees Celsius and are checked against -273.15 °C, which no int in degrees Celsius holds.

The check is written as “not less than zero”, as the postcondition of abs() in Non-negativity and individual values is, and a NaN isn’t negative, so it passes. Whether a NaN is a valid value is a question of range, which the checking policies of [P4412R0] answer: check_in_range, and check_non_negative attached to an origin, reject it. quiet_NaN() and signaling_NaN() of numeric_limits for a checked point are built without the check, as the NaN of a non-negative absolute is.

A unit conversion keeps the floor, because .in(U) changes only the unit and never the quantity specification. point<isq::thermodynamic_temperature[deg_C]>(20.).in(K) is still checked.

An absolute_point_origin declared by the user introduces a zero of its own, and a point measured from it is never checked against a floor. A net reading on a laboratory balance is measured from the tare, so readings below the tare are negative and valid:

inline constexpr struct tare : absolute_point_origin<isq::mass> {} tare;

quantity net = tare - 0.3 * kg;  // OK: 0.3 kg below the tare

A signed position along an axis uses the same pattern: an origin of its own, or a signed quantity such as altitude. So does a time instant. ISO 80000-3 defines duration as the “measure of the time difference between two events” and notes that “duration is often just called time”. The second is a unit of that non-negative kind, so point<s>(-1.), measured from the natural origin, is a contract violation. An instant before an epoch is measured from the epoch, as in epoch + delta<s>(-1.), where epoch is an absolute_point_origin of the program.

5.2 Domain checks in generic code

When a function takes an amount, the query lets it check its input without knowing which quantity it was given:

template<auto R, typename Rep>
constexpr bool in_domain(const quantity<R, Rep>& q)
{
  if constexpr (is_non_negative(get_quantity_spec(R)))
    return !(q < quantity<R, Rep>::zero());
  else
    return true;
}

template<quantity_of<isq::mechanical_work> Q>
void log_energy_transfer(Q w)
  pre(in_domain(w))
{
  // ...
}

log_energy_transfer(isq::mechanical_work(-12.0 * J));  // OK: a work may be negative
log_energy_transfer(isq::kinetic_energy(-12.0 * J));   // contract violation
log_energy_transfer(-12.0 * J);                        // OK: kind_of<isq::energy> is signed

This is the case from Preconditions as a symptom that a hand-written precondition can’t handle. The knowledge of which energies are signed lives in the definitions of isq::mechanical_work and isq::kinetic_energy, and the function picks it up from the argument type.

The function still states a precondition. It is now written once, the same way for every quantity.

The helper is correct only where the author knows the parameter is an amount. A delta of a non-negative quantity may be negative (a mass lost in a reaction), and with deltas only, its type doesn’t say so. -12.0 * m is such a delta in [P3045R10]: the library doesn’t check it, while in_domain rejects it, because kind_of<isq::length> takes the domain of length.

5.3 What the tag doesn’t do

With only deltas and points, the tag doesn’t change how a delta behaves:

These gaps come from the missing abstraction. A delta is the right type for a difference, and the library has no other type for an amount measured from a true zero. [P4185R2] proposes that abstraction, the absolute quantity, and Enforcement on absolute quantities describes how the tag would be checked on its values. This paper doesn’t depend on it.

6 Enforcement on absolute quantities

This section applies only if absolute quantities [P4185R2] are adopted. Nothing else in this paper depends on them.

[P4185R2] proposes a third abstraction, the absolute quantity: an amount measured from the true zero of its quantity. quantity<isq::mass[kg]> becomes an absolute, and a difference of two masses is a quantity<delta<isq::mass[kg]>>. An absolute of a non-negative quantity is the amount that non_negative describes, so with absolute quantities the tag can be checked on values, not only on points. [P4185R2] leaves these questions to this paper.

6.1 Where the check runs

A value is checked only when it is stored in an absolute whose quantity specification is non-negative: a named non-negative specification, or a quantity kind whose root is non-negative. A delta is never checked, and neither is an expression (see Expressions carry no flag).

A value written with a bare unit is an absolute of a quantity kind, so it is checked when the root of the kind is non-negative. quantity x = -10. * kg carries kind_of<isq::mass> and is a contract violation, and so are -1 * kg and -5. * K, while -5. * A is not, because an electric current can be negative. A signed value of a kind is spelled as a delta: delta<kg>(-10.), or delta<K>(-5.) for a temperature change.

A vector or tensor value written with a bare unit, as in vec * (m / s), is ill-formed. The representation type doesn’t choose the abstraction of a quantity, as [P4185R2] already requires for a named specification, so such a value doesn’t silently become a delta of the kind. It is written as delta<m / s>(vec), or with a named vector specification, as in vec * isq::velocity[m / s], which is a delta already.

The check runs at four kinds of sites:

  1. Construction from a number, -10. * kg included, or from a value whose specification is not non-negative: from an expression, as in isq::mass_density(mass / volume), or from a possibly_negative specification, as in the implicit conversion of an effective_mass to a mass.
  2. .absolute() on a delta.
  3. Addition and subtraction of anything but a non-negative absolute, +=, -=, and -- included. The result is still an absolute, and the other operand can take it below zero.
  4. Multiplication and division by a number, *= and /= included: -1.0 * mass is still a mass. Negation is not such a site: [P4185R2] makes the negation of a non-negative absolute a delta, so -mass is a delta.

The check is skipped where the operands already guarantee the sign:

The skips are decided from the types alone, so they assume that the arithmetic of the representation doesn’t overflow. In the prototype, an integer representation that overflows in a sum, in ++, or in a unit conversion can wrap below zero without a check (for example, += on a std::int8_t mass). This is a gap of the prototype and not proposed behavior.

quantity load    = 10. * kg;                                   // site 1: kind_of<isq::mass>
quantity cargo   = isq::mass(load);                            // not checked: load is non-negative
quantity copy    = cargo;                                      // not checked
quantity fuel    = isq::mass(500. * kg);                       // site 1
fuel -= delta<isq::mass[kg]>(150.);                            // site 3: 350 kg
fuel *= 2.;                                                    // site 4: 700 kg
quantity spare   = (fuel - cargo).absolute();                  // site 2: 690 kg
quantity deficit = fuel.delta() - delta<isq::mass[kg]>(800.);  // a delta, not checked: -100 kg

A negative value is a contract violation, as a point below its floor is (see Checked points), so it follows the evaluation semantic in effect, during constant evaluation as well. If the representation has the constraint_violation_handler of [P4412R0], a negative value is reported to the handler instead, and if the handler returns, the value is stored. The check is written as !(value < 0), so a NaN, which isn’t negative, passes it, as it passes the floor of a point.

A negative value can be stored in a delta (deficit in the example), or in an absolute of a signed quantity, such as an electric current or a quantity declared possibly_negative. A bare unit doesn’t avoid the check.

6.2 Why the checks can’t be elided further

The quotient of two non-negative absolutes is never negative, so it is tempting to skip the check when such a quotient becomes a named quantity:

quantity<isq::mass_density[kg / m3]> density(quantity<isq::mass[kg]> mass, quantity<isq::volume[m3]> volume)
{
  return isq::mass_density(mass / volume);  // checked
}

mass / volume itself isn’t checked, because its specification is an expression (see Expressions carry no flag). The check runs when the quotient becomes a mass density. Skipping it would need the type of mass / volume to prove the sign, and it can’t: -1.0 * (mass / volume) has the same type. A correct computation can also give a negative value of a type built only from non-negative quantities. The mass flow rate through a surface is ρvAcos⁡θ\rho v A \cos\theta. Mass density, speed, and area are non-negative, and the rate is negative when the fluid crosses the surface in the opposite direction:

quantity flow = rho * v * A * std::cos(theta);  // theta in radians, as a double; negative for theta > 90°
static_assert(std::is_same_v<decltype(flow), decltype(rho * v * A)>);

A check at * that trusts non-negative factors rejects flow, a valid value. Skipping the check where the value enters a named non-negative quantity instead accepts isq::mass_density(-1.0 * (mass / volume)). A product with a number, whose sign is unknown, could be made a delta, as negation of a non-negative absolute is in [P4185R2], but [P4185R2] keeps a product of an absolute and a number an absolute, so that 2. * fuel stays an amount. An absolute of an expression can also be built directly from a negative number.

Inference from defining equations doesn’t change this. It decides at compile time that mass density is non-negative, so isq::mass_density(...) checks. It says nothing about the value passed to it.

7 Why the tag should come before the ISQ definitions

[P3045R10] proposes the framework and no system of quantities. [P4213R1] proposes the ISQ and SI definitions needed for the SI, and its synopsis already uses non_negative with a note that the tags can be dropped if this feature is not accepted. Deciding the feature first matters for three reasons.

  1. Adding a domain later changes the behavior of valid programs. Once a quantity<point<isq::length[m]>> measured from the natural origin is allowed to hold -1 m, tagging isq::length in a later standard turns that program into a contract violation if points measured from a natural origin are checked (the third poll). So does point<m>(-1.), because the kind of the metre takes the domain of length.

  2. The domain shapes the hierarchy. In [mp-units], height is a non-negative child of a signed altitude, so a height converts implicitly to an altitude and not the other way round. With no domain to express, the natural shape is two siblings under length, with different conversions. A conversion that is allowed in one standard can’t be removed later without breaking code.

  3. The decisions need review, quantity by quantity. They are not obvious. Until recently [mp-units] tagged energy and power as non-negative, so mechanical work, which is negative when the force opposes the displacement, inherited the error. So did the Gibbs energy G=H−TSG = H - TS, which is negative whenever TSTS exceeds HH. The fix removed non_negative from both roots. The same review removed it from specific energy, whose children were wrong as well: specific internal energy and specific enthalpy are measured from a chosen reference state and are negative below it, and specific Gibbs energy g=h−Tsg = h - Ts is negative whenever TsTs exceeds hh. Heat capacity (negative for self-gravitating systems), energy density, and the damping coefficient (negative for a self-excited oscillation) need the same review. Errors of this kind are easy to make, and LEWG reviewing each definition once, with its domain, is the cheapest point to catch them. [P4213R1] lists 40 ISQ names, and later parts of the ISQ add several hundred. Reviewing each one for its domain in a second pass repeats the work.

The feature is a pure addition to [P3045R10]: a named_quantity_spec already accepts properties that are not quantity specifications (subkind and the character values), and the two tags are two more.

8 Inference from defining equations

This section describes a design that has been prototyped against the definitions of [mp-units] but is not implemented there. It is polled separately, and the tag is useful without it.

Non-negativity can be propagated to a large share of the derived quantities. A named derived quantity has a defining equation, and in many cases the equation already proves the domain: mass density is mass over volume, and both are non-negative. Inference makes the tag on mass density redundant, and with it most of the explicit tags in the ISQ.

8.1 The rule

A defining equation is non-negative if and only if it has at least one factor and every factor is a non-negative named quantity specification, raised to any power. A named quantity with a defining equation is non-negative if:

possibly_negative cancels the inherited and the inferred property of the quantity that carries it. It doesn’t cancel inference from the equation of a descendant: a child of energy whose defining equation is mass * pow<2>(length) / pow<2>(duration) infers non_negative from it, and needs possibly_negative of its own if it is signed. If the tag blocked inference in the whole subtree, every non-negative energy would need its own tag again, kinetic energy among them.

Inference needs the tag on two roots as well. The defining equations of energy and power in [mp-units], ML2T−2M L^2 T^{-2} and ML2T−3M L^2 T^{-3}, have only non-negative factors, so both would infer non_negative, and so would every signed energy and power below them. Adopting inference therefore means marking both roots possibly_negative. Without inference, the tag on a root has no effect, and this paper doesn’t propose it there.

8.2 Two cases that break a simpler rule

An earlier attempt at inference in [mp-units] failed on two cases, and the rule above avoids both.

The first is the identity of the algebra. dimensionless is the empty product. A fold over zero factors that starts from “non-negative” marks dimensionless as non-negative, then every dimensionless quantity, and then every factor that should carry a sign. The empty product must count as signed: dimensionless denotes any quantity of dimension one, whose domain is the whole real line.

The second is an even power. It is tempting to treat Q2Q^2 as non-negative for any real QQ. The type algebra, however, folds the product of two independent values of one quantity into the same type as a square:

inline constexpr struct signed_coefficient : named_quantity_spec<dimensionless> {} signed_coefficient;

quantity q1 = signed_coefficient(0.8 * one);
quantity q2 = signed_coefficient(-0.5 * one);
quantity prod = q1 * q2;  // value: -0.4
static_assert(prod.quantity_spec == pow<2>(signed_coefficient));

The type doesn’t record whether its two factors were one value, so the rule treats Q2Q^{2} like QQ. In the ISQ definitions of [mp-units] this costs one explicit tag: non-active power is defined through the S2\sqrt{S^2} idiom, and the tag isn’t there yet. Kinetic energy is not such a case. It is non-negative because mass and speed are.

8.3 Signs that no dimension shows

Some signed quantities have equations whose dimensional factors are all non-negative. Reactive power is U⋅I⋅sin⁡φU \cdot I \cdot \sin\varphi, and the sign comes from the phase angle. A dimensional formula shows only ML2T−3M L^2 T^{-3}.

The signed_coefficient above handles such cases. It is a named child of dimensionless, so it survives simplification (only dimensionless itself folds away), and because no even power is trusted, it can’t be squared away. Placed in a defining equation, it records that a factor of unknown sign takes part. One coefficient serves sin\sin, cos\cos, a negation, or a signed scale factor, because the rule needs to know only that some factor is signed. The cost is that a product without the coefficient, such as U * I, then converts to such a quantity only explicitly.

Negating an already signed quantity needs nothing. The Massieu function J=−A/TJ = -A/T negates the Helmholtz energy, which is signed because energy is, so A/TA/T infers a signed result with no annotation.

8.4 Audit of the reference implementation

The rule was run against every named quantity specification of the ISQ in the V3 branch of [mp-units], by compiling a patched copy of the library and comparing the property of each quantity with and without inference. The run marked energy and power possibly_negative, as The rule requires, and predates the removal of non_negative from specific energy.

Item
Count
Named quantity specifications 390
Explicit non_negative tags 134
Tags made redundant by inference 76
Quantities whose property changes 21

All 21 changes go from signed to non-negative, as the rule guarantees. They fall into three groups:

With these tags added, only refractive index and logarithmic decrement still change.

An earlier run of the prototype against older definitions found 389 quantities, 109 tags, 90 redundant, and 27 changes. That run still saw energy and power as non-negative, so it also found the errors that have since been fixed by hand. Two conclusions drawn from that run don’t hold for the current definitions: electromagnetic energy density needs no tag of its own, because it inherits from energy density, and non-active power and mechanical energy don’t carry the explicit tags those conclusions assumed.

8.5 Call-site arithmetic

Inference applies to defining equations only. The expression types of Expressions carry no flag stay unflagged, because the raw numbers in an expression are invisible to the type.

Dividing a non-negative quantity by another one gives a non-negative result. For a named quantity the type shows it: the definition of mass density proves its domain at compile time, which is what inference does. For a value computed in user code, it can’t. mass / volume and -1.0 * (mass / volume) share a type, so the type can’t carry the proof. A quotient of deltas is a delta, and deltas are not checked, so there is nothing to skip. With absolute quantities, the checks that can be skipped are listed in Where the check runs, and Why the checks can’t be elided further explains why the others stay.

9 Review feedback

At St. Louis 2024, a participant considered the tag a small addition that is worth adding, with no need to wait for contracts. The tag, the query, and the point check are implemented without contracts. The check is a precondition in [mp-units] and can use contracts where they are available. At the same meeting, another participant pointed out that speed is often used for the signed component of a velocity along an axis, and raised it again at Sofia 2025. ISO 80000-3 defines speed as the magnitude of velocity, so the ISQ speed stays non-negative. A signed velocity along one axis, such as a rate of climb, is declared as a child of isq::velocity and stored in a scalar representation. Its vector character keeps it signed, also under inference (see Inheritance through the hierarchy), and it uses m / s.

At Sofia 2025, a participant argued that runtime checks are hard to add later and that the property should be in the type, and another noted that preconditions can be turned off. The property is in the type (see The non_negative tag), and the check on points (see Checked points) is a precondition, so it follows the evaluation semantic in effect.

At Croydon 2026, a participant asked that the response to a negative value for a non-negative quantity be customizable. The constraint_violation_handler of [P4412R0] provides that for points (see Checked points), and with absolute quantities for absolutes as well (see Where the check runs).

At Brno 2026, SG6 reviewed [P4185R1]:

10 Impact on other papers

[P3045R10]. The synopsis gains two property tags next to subkind and one query:

namespace std {

inline constexpr struct non_negative {} non_negative;
inline constexpr struct possibly_negative {} possibly_negative;

template<quantity_spec Q>
consteval bool is_non_negative(Q);

}

The four specializations of named_quantity_spec gain the rules of Inheritance through the hierarchy, kind-of-t<QS> reports the property of QS, and point<QS> and derived_quantity_spec report false. The operations that store a point gain the precondition of Checked points.

Natural origins change in three ways:

[P4412R0]. The natural origin of a non-negative quantity doesn’t get check_non_negative: the floor belongs to the quantity specification and needs no policy of an origin. A point of a quantity kind with a non-negative root, such as point<m>(5.), is checked as well. The floor is checked as an inherited checking bound is (see the rule for bounds the point can hold in [P4412R0]): exactly, in the common type of the value and the offset to the natural origin, so point<deg_C>(-273) with an int representation is valid and point<deg_C>(-274) is not. When the origin of a floored point has a policy, the policy runs first and the floor checks the value to be stored, and the range of the policy may reach below the true zero. An inherited adjusting policy and the min() and max() of numeric_limits for a point still need the offset and the bounds to be exact in the point’s own type.

[P4213R1]. si::absolute_zero is declared as the natural origin of thermodynamic temperature under its own name, and the kelvin names it as its origin. si::ice_point stays a relative origin and inherits the floor through its chain. [P4213R1] also marks energy possibly_negative, which has an effect only under inference (see The rule), so the tag belongs with that poll. A later revision should review the domain of every definition it lists, which is easier once SG6 has given a direction on this paper.

[P4185R2]. This paper replaces the chapter of [P4185R1] on non-negative quantities. [P4185R2] no longer contains it and doesn’t depend on non-negativity. If both papers are adopted, absolute quantities are checked as described in Enforcement on absolute quantities: a value written with a bare unit is an absolute of its kind and is checked when the root of the kind is non-negative, and a vector or tensor value can’t be written with a bare unit and is spelled as a delta.

11 Implementation experience

[mp-units] implements non_negative, possibly_negative, is_non_negative, inheritance, and the automatic check on points of a named non-negative quantity measured from its natural origin. The behavior is the same on its master branch and on its V3 branch, which uses the spelling of [P3045R10]. On master a point is spelled quantity_point. The feature is not yet in a released version of [mp-units]: it is scheduled for 2.6.0. On both branches, a quantity kind is never non-negative and si::absolute_zero is an origin of its own, so point<m>(-1.) and point<isq::thermodynamic_temperature[K]>(-1.) are not checked there.

The ISQ definitions of [mp-units] carry 133 non_negative tags and 2 possibly_negative tags (effective mass and, for now, altitude), and the test suite checks the property of every quantity.

The prototype of absolute quantities that [P4185R2] describes, a branch of [mp-units] off its V3 branch that is not yet public, implements the rules of this revision. A quantity kind takes the domain of its root, si::absolute_zero is the natural origin of thermodynamic temperature, and a point is checked as described in Checked points, exactly, with the NaN of numeric_limits built without the check. The prototype also implements the checks and skips of Where the check runs, with the gaps described there. The check on values written with a bare unit is implemented, and so is the rule that makes a vector multiplied by a bare unit ill-formed. A complex value of a kind isn’t checked, because a complex quantity has no order. The examples of Checked points and Where the check runs run as commented with GCC 15, using gsl-lite as the contract backend.

Inference from defining equations is not implemented. The audit in Audit of the reference implementation patched a copy of the library to compute the inferred property.

12 Open questions

  1. Should possibly_negative on a vector, tensor, or complex quantity be ill-formed? On a root whose defining equation has only non-negative factors, such as energy, the tag is redundant today, but it becomes necessary under inference. On a vector, tensor, or complex quantity it is never necessary. [mp-units] accepts it. non_negative on such a quantity is ill-formed.
  2. The spelling of signed_coefficient. It exists only in the prototype, and the name is a placeholder.

13 Straw polls

  1. SG6 recommends adding the non_negative and possibly_negative properties of quantity_spec, with inheritance and the is_non_negative query as proposed in this paper, to the quantities and units library.
  2. SG6 recommends that the ISQ definitions proposed for standardization state the domain of each quantity with these properties.
  3. A point of a non-negative quantity, a quantity kind with a non-negative root included, measured from a natural origin should be checked, as described in Checked points.
  4. If absolute quantities [P4185R2] are adopted, an absolute of a non-negative quantity, a value written with a bare unit of a non-negative kind included, should be checked as described in Enforcement on absolute quantities.
  5. Encourage more work on inferring non-negativity from defining equations, as described in this paper.

14 Acknowledgements

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 would like to thank Oliver Rosten for raising non-negativity as a missing part of the design and for the discussions that shaped it.

15 References

[Braun2013] S. Braun et al. Negative Absolute Temperature for Motional Degrees of Freedom. Science 339, 52 (2013).
https://doi.org/10.1126/science.1227831
[GSL] Guidelines Support Library.
https://github.com/microsoft/GSL
[mp-units] mp-units - A Physical Quantities and Units library for C++.
https://mpusz.github.io/mp-units
[mp-units quantity safety] Mateusz Pusz. 2025. Bringing Quantity-Safety To The Next Level.
https://mpusz.github.io/mp-units/latest/blog/2025/01/15/bringing-quantity-safety-to-the-next-level
[P2900R14] Joshua Berne, Timur Doumler, Andrzej Krzemieński. 2025-02-14. Contracts for C++.
https://wg21.link/p2900r14
[P3045R10] Mateusz Pusz. Quantities and units library.
https://wg21.link/p3045r10
[P4185R1] Mateusz Pusz. 2026-06-12. Completing the Mathematical Model for C++ Quantities and Units.
https://wg21.link/p4185r1
[P4185R2] Mateusz Pusz. Absolute Quantities.
https://wg21.link/p4185r2
[P4213R1] Mateusz Pusz. Minimal SI: Defining the International System of Units for the C++ Standard Library.
https://wg21.link/p4213r1
[P4412R0] Mateusz Pusz. Range-Validated Quantity Points.
https://wg21.link/p4412r0
[Purcell1951] E. M. Purcell and R. V. Pound. A Nuclear Spin System at Negative Temperature. Physical Review 81, 279 (1951).
https://doi.org/10.1103/PhysRev.81.279
[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
[Shelby2001] R. A. Shelby, D. R. Smith, and S. Schultz. Experimental Verification of a Negative Index of Refraction. Science 292, 77 (2001).
https://doi.org/10.1126/science.1058847