@@ -86,8 +86,8 @@ current model, motivating the need for a new abstraction.
8686
87871 . ** Limited arithmetic for points** – Points can’t be multiplied, divided, or accumulated.
8888 This often forces the users to convert the quantity point to a delta with either
89- ` qp.quantity_from_zero () ` or ` qp.quantity_from(some_origin) ` member functions, which is
90- at least cumbersome.
89+ ` qp.quantity_from_unit_zero () ` or ` qp.quantity_from(some_origin) ` member functions,
90+ which is at least cumbersome.
91912 . ** No text output for points** – A point's textual representation depends on its origin,
9292 which is often implicit or user-defined. As of today, we do not have the means to
9393 provide a text symbol for the point origin. Moreover, points may contain both an
@@ -139,18 +139,18 @@ current model, motivating the need for a new abstraction.
139139 quantity_point<kg> total)
140140 {
141141 gsl_Expects(is_gt_zero(total));
142- return water_lost / total.quantity_from_zero ();
142+ return water_lost / total.quantity_from_unit_zero ();
143143 }
144144
145145 quantity_point<kg> initial[] = { point<kg>(2.34), point<kg>(1.93), point<kg>(2.43) };
146146 quantity_point<kg> dried[] = { point<kg>(1.89), point<kg>(1.52), point<kg>(1.92) };
147147
148- auto point_plus = [](QuantityPoint auto a, QuantityPoint auto b){ return a + b.quantity_from_zero (); };
148+ auto point_plus = [](QuantityPoint auto a, QuantityPoint auto b){ return a + b.quantity_from_unit_zero (); };
149149 quantity_point total_initial = std::reduce(std::cbegin(initial), std::cend(initial), point<kg>(0.), point_plus);
150150 quantity_point total_dried = std::reduce(std::cbegin(dried), std::cend(dried), point<kg>(0.), point_plus);
151151
152- std::cout << "Initial product mass: " << total_initial.quantity_from_zero () << "\n";
153- std::cout << "Dried product mass: " << total_dried.quantity_from_zero () << "\n";
152+ std::cout << "Initial product mass: " << total_initial.quantity_from_unit_zero () << "\n";
153+ std::cout << "Dried product mass: " << total_dried.quantity_from_unit_zero () << "\n";
154154 std::cout << "Moisture content change: " << moisture_content_change(total_initial - total_dried, total_initial) << "\n";
155155 ```
156156
@@ -891,43 +891,77 @@ with:
891891``` cpp
892892quantity temp_cold = point<K>(300 .);
893893quantity temp_hot = point<K>(500 .);
894- quantity carnot_eff_1 = 1 . - temp_cold.quantity_from_zero () / temp_hot.quantity_from_zero ();
895- quantity carnot_eff_2 = (temp_hot - temp_cold) / temp_hot.quantity_from_zero ();
894+ quantity carnot_eff_1 = 1 . - temp_cold.quantity_from_unit_zero () / temp_hot.quantity_from_unit_zero ();
895+ quantity carnot_eff_2 = (temp_hot - temp_cold) / temp_hot.quantity_from_unit_zero ();
896896```
897897
898898It worked, but was far from being physically pure and pretty.
899899
900900
901- ### Why Not Just Use ` (T − T₀) ` as a Workaround ?
901+ ### Why Obvious Workarounds Fall Short ?
902902
903- A common suggestion is to work around the absence of a distinct Absolute type by
904- replacing every absolute temperature $T$ with the explicit expression $(T - T_0)$, where
905- $T_0$ is a ` quantity_point ` at absolute zero. For example:
903+ Two workaround approaches exist, each with its own caveat.
904+
905+ #### Approach 1: ` quantity_from_unit_zero() `
906+
907+ ` quantity_from_unit_zero() ` returns the displacement of a quantity point from its unit's
908+ origin. For Kelvin this is ` si::absolute_zero ` , so the result is exactly the
909+ thermodynamic temperature:
910+
911+ ``` cpp
912+ point<K>(294.15 ).quantity_from_unit_zero(); // 294.15 K ✓
913+ ```
914+
915+ For Celsius, however, the unit's origin is ` si::ice_point ` — the _ ice point_ .
916+ The function therefore returns the displacement from the ice point, not from absolute
917+ zero:
906918
907919``` cpp
908- quantity<point<K>> temp_cold = point<K>(300 .);
909- quantity<point<K>> temp_hot = point<K>(500 .);
910- // Force deltas from zero explicitly every time:
911- quantity carnot_eff = 1 . - temp_cold.quantity_from_zero() / temp_hot.quantity_from_zero();
920+ point<deg_C>(21 ).quantity_from_unit_zero(); // 21 ℃ — displacement from ice point, not 294.15 K!
912921```
913922
914- While this produces the correct numerical answer, it has several drawbacks:
915-
916- 1 . ** Manual burden** — the user must remember to apply the ` quantity_from_zero() ` call
917- every time a thermodynamic formula requires ratio-scale semantics.
918- 2 . ** No call-site enforcement** — generic function interfaces cannot require an
919- Absolute at the type level; a ` quantity_point<deg_C> ` can slip through silently.
920- 3 . ** Lost semantic intent** — the type ` quantity_point<K> ` says "a location on the
921- temperature scale," not "a thermodynamic energy magnitude." The distinction between
922- an interval-scale location and a ratio-scale magnitude disappears.
923- 4 . ** Verbose code** — the workaround turns clean physics equations into manual
924- conversions, defeating the purpose of a high-level units library.
925-
926- The V3 Absolute Quantity abstraction internalizes this conversion. ` 300 * K ` is an
927- Absolute Quantity by default and is directly usable in multiplicative expressions.
928- When an offset-unit measurement must enter a ratio-scale equation, the explicit
929- ` .in(K).absolute() ` chain makes the conversion visible and type-safe — exactly once,
930- at the boundary, rather than scattered throughout the codebase.
923+ This is a silent pitfall: the call compiles and returns a plausible-looking value for any
924+ temperature unit, but is only thermodynamically meaningful when the point is already in
925+ Kelvin. Explicit conversion before the call fixes it:
926+
927+ ``` cpp
928+ point<deg_C>(21 ).in(K).quantity_from_unit_zero(); // 294.15 K ✓
929+ ```
930+
931+ #### Approach 2: Subtract ` si::absolute_zero `
932+
933+ Subtracting the absolute-zero origin always gives the correct thermodynamic temperature
934+ regardless of the unit the point was stored in:
935+
936+ ``` cpp
937+ quantity_point temp = point<deg_C>(21 );
938+ quantity T = temp - si::absolute_zero; // always correct
939+ ```
940+
941+ The only thing to be aware of is the resulting unit. Because ` si::ice_point ` (the origin
942+ of the Celsius scale) is defined internally in ` milli<kelvin> ` , the subtraction yields
943+ ` mK ` rather than ` K ` :
944+
945+ ``` text
946+ T == 294150 mK // correct value, can be rescaled with .in(K)
947+ ```
948+
949+ The value is correct and rescales cleanly to any unit. In a physical equation like the
950+ ideal gas law, ` p ` will come out in ` mPa ` instead of ` Pa ` , but it will convert
951+ automatically on the first assignment to a typed quantity such as ` quantity<Pa> ` .
952+
953+ #### The bottom line
954+
955+ Both approaches work correctly when used with care. ` quantity_from_unit_zero() ` is concise but
956+ requires the point to already be in Kelvin; subtraction from ` si::absolute_zero ` is
957+ always safe but carries an ` mK ` unit until rescaled. In either case, the right idiom —
958+ ` .in(K).quantity_from_unit_zero() ` — must be remembered and applied at every call site, and
959+ there is no way to enforce it through the type system.
960+
961+ V3 Absolute Quantities address this: ` 300 * K ` is already an Absolute Quantity, directly
962+ usable in any multiplicative expression. When an offset-unit point must enter a
963+ thermodynamic equation, the explicit ` .in(K).absolute() ` chain makes the conversion
964+ visible and type-safe — exactly once, at the boundary.
931965
932966
933967### Design Philosophy and Standardization
0 commit comments