Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions docs/examples/measurement.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,23 @@ projects:

### Integration with mp-units

To use a custom type as a quantity representation, it must satisfy the `RepresentationOf`
concept. The library verifies this at compile time:
To use `measurement<T>` as a quantity representation, two things must be provided.

First, a `scaling_traits` specialization that teaches the library how to scale a
`measurement<T>` to a `measurement<U>` by a unit magnitude:

```cpp title="measurement.cpp"
--8<-- "example/measurement.cpp:46:53"
```

The `scale<U>(M, value)` call recurses into the built-in `scaling_traits` for the
underlying scalar type `T` → `U`, so uncertainty propagation is exact and type-safe.

Second, the library verifies at compile time that `measurement<T>` satisfies the
`RepresentationOf` concept:

```cpp title="measurement.cpp"
--8<-- "example/measurement.cpp:47:48"
--8<-- "example/measurement.cpp:55:58"
```

This allows `measurement<T>` to be used seamlessly with any **mp-units** quantity type.
Expand All @@ -69,7 +81,7 @@ This allows `measurement<T>` to be used seamlessly with any **mp-units** quantit
The example demonstrates various uncertainty propagation scenarios:

```cpp title="measurement.cpp"
--8<-- "example/measurement.cpp:50:77"
--8<-- "example/measurement.cpp:62:85"
```

This showcases:
Expand Down
108 changes: 89 additions & 19 deletions docs/how_to_guides/integration/using_custom_representation_types.md
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ public:

---

#### `treat_as_floating_point<Rep>`
#### `treat_as_floating_point<Rep>` { #treat_as_floating_point }

A specializable variable template that tells the library whether a type should be treated as
floating-point for the purpose of allowing implicit conversions:
Expand Down Expand Up @@ -300,40 +300,53 @@ for details on how this affects implicit conversions between quantities

---

#### `is_value_preserving<From, To>`
#### `implicitly_scalable<FromUnit, FromRep, ToUnit, ToRep>` { #implicitly_scalable }

A specializable variable template that determines whether a conversion from one representation
type to another preserves values:
A specializable variable template that controls whether a conversion from
`quantity<FromUnit, FromRep>` to `quantity<ToUnit, ToRep>` is implicit or requires an
explicit cast via `value_cast`/`force_in`:

```cpp
template<typename From, typename To>
constexpr bool mp_units::is_value_preserving =
treat_as_floating_point<To> || !treat_as_floating_point<From>;
template<auto FromUnit, typename FromRep, auto ToUnit, typename ToRep>
constexpr bool mp_units::implicitly_scalable =
std::is_convertible_v<FromRep, ToRep> &&
(treat_as_floating_point<ToRep> ||
(!treat_as_floating_point<FromRep> && is_integral_scaling(FromUnit, ToUnit)));
```

**Default behavior:** A conversion is value-preserving if:
**Default behavior:** A conversion is implicit iff all of the following hold:

- The destination type is floating-point (can represent any source value), OR
- The source type is not floating-point (integer → integer or integer → float is safe)
- `FromRep` is convertible to `ToRep`, AND
- one of:
- `ToRep` is floating-point (absorbs any numeric value without truncation), OR
- neither rep is floating-point AND the unit magnitude ratio is an integral factor
(e.g. `m → mm`: ×1000), as reported by `mp_units::is_integral_scaling(from, to)`

This follows the same practice as `std::chrono::duration` conversions.
`mp_units::is_integral_scaling(from, to)` is a `consteval` predicate you can also use
in your own specializations to distinguish the integral-factor case from fractional ones
(e.g. `mm → m`: ÷1000, `ft → m`, `deg → rad`).

**When to specialize:** If you have custom types with specific value preservation semantics:
Conversions with a fractional factor are always explicit for integer reps.

**When to specialize:** If your custom type has different implicit-conversion semantics:

```cpp
// Example: my_decimal has more precision than double
template<>
constexpr bool mp_units::is_value_preserving<my_decimal, double> = false;
// my_decimal is safe to receive from double implicitly, but double cannot losslessly
// represent my_decimal (more precision), so that direction stays explicit.
template<auto FromUnit, auto ToUnit>
constexpr bool mp_units::implicitly_scalable<FromUnit, double, ToUnit, my_decimal> = true;

template<>
constexpr bool mp_units::is_value_preserving<double, my_decimal> = true;
template<auto FromUnit, auto ToUnit>
constexpr bool mp_units::implicitly_scalable<FromUnit, my_decimal, ToUnit, double> = false;
```

**Impact:** Controls whether conversions are implicit or require explicit casts.
**Impact:** Controls whether conversions between quantity types are implicit or require
`value_cast`/`force_in`. See [Value Conversions](../../users_guide/framework_basics/value_conversions.md)
for the full picture.

---

#### `representation_values<Rep>`
#### `representation_values<Rep>` { #representation_values }

A specializable class template that provides special values for a representation type:

Expand Down Expand Up @@ -384,6 +397,63 @@ struct mp_units::representation_values<my_custom_type<T>> {
- Mathematical operations like `floor()`, `ceil()`, `round()`
- Division by zero checks

---

#### `scaling_traits<From, To>` { #scaling_traits }

A class template specialization that defines how a value of type `From` is scaled by a
unit magnitude to produce a value of type `To`. Built-in support is provided for all
standard floating-point and integral types.

To support a custom representation type, specialize `mp_units::scaling_traits` for your
type:

```cpp
template<typename T, typename U>
struct mp_units::scaling_traits<MyType<T>, MyType<U>> {
template<auto M>
[[nodiscard]] static constexpr MyType<U> scale(const MyType<T>& value) { ... }
};
```

The `mp_units::scale<To>(M, value)` free function calls
`scaling_traits<From, To>::template scale<M{}>(value)`, and is the primary way the
library scales values during unit conversions. A helper `mp_units::silent_cast<To>(value)`
performs a `static_cast` with truncating conversion warnings suppressed — useful when
you need to cast the scaled result to the target type.

To control whether a particular conversion is implicit or explicit, specialize
[`mp_units::implicitly_scalable<>`](#implicitly_scalable) separately —
`scaling_traits::scale<M>()` is responsible for *how* to scale, not *whether* to do so
implicitly.

Once a `scaling_traits` specialization is provided, the custom type automatically
satisfies the `MagnitudeScalable` concept and can be used as the representation type of a
`quantity`.

??? example "`measurement<T>`"

A `measurement<T>` type carries both a value and an uncertainty. Scaling a measurement
must apply the same factor to both components:

```cpp
template<typename T, typename U>
struct mp_units::scaling_traits<measurement<T>, measurement<U>> {
template<auto M>
[[nodiscard]] static constexpr measurement<U> scale(const measurement<T>& value)
{
return measurement<U>(
mp_units::scale<U>(M, value.value()),
mp_units::scale<U>(M, value.uncertainty()));
}
};
```

```cpp
static_assert(mp_units::RepresentationOf<measurement<int>, mp_units::quantity_character::real_scalar>);
static_assert(mp_units::RepresentationOf<measurement<double>, mp_units::quantity_character::real_scalar>);
```


## Built-in Support

Expand Down
2 changes: 1 addition & 1 deletion docs/reference/systems_reference/.cache.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"source_hash": "779142616607efd3425c82a296b4f54add054f497074117c5ae9581b19e88582"
"source_hash": "d2564890eceb1ea7a6c23336abea40ab1620cdbeec04c5053dca9269ab3c2db0"
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ truncation and is blocked - even without changing units!
You can customize this behavior with:

- `treat_as_floating_point<Rep>`: Tells the library if a type should be treated as floating-point
- `is_value_preserving<From, To>`: Determines if a conversion preserves values
- `implicitly_scalable<FromUnit, FromRep, ToUnit, ToRep>`: Controls whether a specific conversion is implicit or explicit

By default, **mp-units** uses `std::chrono::duration`-like logic for these.

Expand Down
113 changes: 99 additions & 14 deletions docs/users_guide/framework_basics/value_conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Changing any of the above may require changing the value stored in a quantity.
## Value-preserving conversions

```cpp
auto q1 = 5 * km;
quantity q1 = 5 * km;
std::cout << q1.in(m) << '\n';
quantity<si::metre, int> q2 = q1;
```
Expand All @@ -28,42 +28,45 @@ the one measured in meters.
In case a user would like to perform an opposite transformation:

```cpp
auto q1 = 5 * m;
quantity q1 = 5 * m;
std::cout << q1.in(km) << '\n';
quantity<si::kilo<si::metre>, int> q2 = q1;
```

Both conversions will fail to compile.
Both conversions will fail to compile because they try to truncate the quantity value.

There are two ways to make the above work. The first solution is to use a floating-point
representation type:

```cpp
auto q1 = 5. * m;
quantity q1 = 5. * m;
std::cout << q1.in(km) << '\n';
quantity<si::kilo<si::metre>> q2 = q1;
```

or

```cpp
auto q1 = 5 * m;
quantity q1 = 5 * m;
std::cout << q1.in<double>(km) << '\n';
std::cout << value_cast<double>(q1).in(km) << '\n';
quantity<si::kilo<si::metre>> q2 = q1; // double by default
```

!!! important

The **mp-units** library follows [`std::chrono::duration`](https://en.cppreference.com/w/cpp/chrono/duration)
logic and treats floating-point types as value-preserving.
logic and treats floating-point types as implicitly convertible to any unit —
see [`implicitly_scalable`](../../how_to_guides/integration/using_custom_representation_types.md#implicitly_scalable)
for details.


## Value-truncating conversions

The second solution is to force a truncating conversion:

```cpp
auto q1 = 5 * m;
quantity q1 = 5 * m;
std::cout << value_cast<km>(q1) << '\n';
quantity<si::kilo<si::metre>, int> q2 = q1.force_in(km);
```
Expand Down Expand Up @@ -93,7 +96,8 @@ quantity<si::metre, int> q3 = value_cast<int>(3.14 * m);

In some cases, a unit and a representation type should be changed simultaneously. Moreover,
sometimes, the order of doing those operations matters. In such cases, the library provides
the `value_cast<U, Rep>(q)` which always returns the most precise result:
the `value_cast<U, Rep>(q)` and `q.force_in<Rep>(U)` which always return the most precise
result:

=== "C++23"

Expand Down Expand Up @@ -158,19 +162,21 @@ the `value_cast<U, Rep>(q)` which always returns the most precise result:
```cpp
using namespace unit_symbols;
Price price{12.95 * USD};
Scaled spx = value_cast<USD_s, std::int64_t>(price);
Scaled spx1 = value_cast<USD_s, std::int64_t>(price);
Scaled spx2 = price.force_in<std::int64_t>(USD_s);
```

As a shortcut, instead of providing a unit and a representation type to `value_cast`, you
may also provide a `Quantity` type directly, from which unit and representation type are
taken. However, `value_cast<Quantity>`, still only allows for changes in unit and
representation type, but not changing the type of the quantity. For that, you will have
to use a `quantity_cast` instead.
to use a [`quantity_cast`](simple_and_typed_quantities.md#quantity_cast-to-force-unsafe-conversions)
instead.

Overloads are also provided for instances of `quantity_point`. All variants of
`value_cast<...>(q)` that apply to instances of `quantity` have a corresponding version
applicable to `quantity_point`, where the `point_origin` remains untouched, and the cast
changes how the "offset" from the origin is represented. Specifically, for any
Overloads are also provided for instances of [`quantity_point`](the_affine_space.md#quantity_point).
All variants of `value_cast<...>(q)` that apply to instances of `quantity` have a corresponding
version applicable to `quantity_point`, where the `point_origin` remains untouched, and
the cast changes how the "offset" from the origin is represented. Specifically, for any
`quantity_point` instance `qp`, all of the following equivalences hold:

```cpp
Expand All @@ -191,6 +197,77 @@ origin point may require an addition of a potentially large offset (the differen
the origin points), which may well be outside the range of one or both quantity types.


## Integer scaling: fixed-point arithmetic

When both the source and target representation are integral types, unit conversions with
a non-integer conversion factor (e.g. `deg → grad`, factor 10/9) raise two challenges
that a naive implementation cannot handle correctly:

- **Intermediate overflow** — computing `value × num / den` in `intmax_t` overflows for
large values even when the final result fits in the representation type, producing
silently wrong results:

```cpp
// deg -> grad: factor 10/9
// A naive implementation multiplies first: 1e18 * 10 overflows int64_t (max ≈ 9.22e18):
quantity q = (std::int64_t{1'000'000'000'000'000'000} * deg).force_in(grad);
// Expected: 1'111'111'111'111'111'111ᵍ
// Naive result: -938'527'119'301'061'290ᵍ (silent undefined behaviour)
// mp-units result: 1'111'111'111'111'111'111ᵍ (correct)
```

- **Floating-point dependency** — conversions involving irrational factors (e.g. `deg → rad`,
factor `π/180`) require a `double` intermediate in a naive implementation. This fails
silently on FPU-less embedded targets and loses precision for 64-bit integer values
(a `double` has only 53 bits of mantissa).

Both challenges are addressed by using **fixed-point arithmetic**: the conversion factor is
represented at compile time as a double-width integer constant, so the runtime computation
is a pure integer multiply followed by a right-shift with no risk of intermediate overflow
and no floating-point operations.

??? info "Implementation details"

The library distinguishes three sub-cases based on the magnitude $M$ that relates the
two units:

| Case | Condition | Example | Operation | Conversion |
|-------------------|---------------------------|----------------------------|----------------------|:----------:|
| Integral factor | $M \in \mathbb{Z}^+$ | `m → mm` ($\times 1000$) | `value * M` | implicit |
| Integral divisor | $M^{-1} \in \mathbb{Z}^+$ | `mm → m` ($\div 1000$) | `value / M` | explicit |
| Non-integer ratio | otherwise | `ft → m` ($\times 0.3048$) | fixed-point multiply | explicit |

For the non-integer case the magnitude is converted **at compile time** to a
fixed-point constant with double the bit-width of the representation type. For
example, when scaling a 32-bit integer value, a 64-bit fixed-point intermediate is
used. The actual runtime computation is then a pure integer multiply followed by a
right-shift:

$$
\text{result} = \left\lfloor \text{value} \times \lfloor M \cdot 2^N \rfloor \right\rfloor \gg N
$$

where $N$ equals the bit-width of the source representation type. On platforms where
`__int128` is available (most 64-bit targets), the double-width arithmetic is
implemented natively; on others, a portable `double_width_int` emulation is used in
`constexpr` context.

Because the intermediate is double-width, it cannot overflow as long as the input
value fits in the representation type — a value of `std::int64_t` will never silently
overflow during the multiplication step.

For the non-integer ratio path, the result is **truncated toward zero**. The
fixed-point constant is rounded *away* from zero at compile time to compensate for
one level of double-rounding, keeping the maximum error within 1 ULP of the true
result (i.e. at most ±1 relative to the last bit of the output).

!!! hint

Chained conversions can accumulate this truncation error additively. Where exact
round-trip behavior is required, prefer floating-point representations or perform
conversions in a single step rather than via an intermediate unit.


## Scaling overflow prevention

In the case of small integral types, it is easy to overflow the representation type for
Expand All @@ -215,6 +292,14 @@ representation type. We decided not to allow such conversions for safety reasons
the value of `0 km` would work.


## Custom representation types

For information on how to integrate a custom representation type with the quantity
conversion machinery — including how to provide a `scaling_traits<From, To>` specialization
and `implicitly_scalable<FromUnit, FromRep, ToUnit, ToRep>` — see
[Using Custom Representation Types](../../how_to_guides/integration/using_custom_representation_types.md#scaling_traits).


## Value conversions summary

The table below provides all the value conversion functions that may be run on `x` being the
Expand Down
Loading
Loading