Representation Types¶
Every quantity in mp-units has a representation type that stores the numerical value.
While the library works seamlessly with fundamental arithmetic types (except bool) and
std::complex, you can also use custom representation types to model domain-specific
requirements—such as range-validated values, vectors, or specialized numeric types.
The representation type determines what kind of mathematical operations are available and how the quantity behaves in calculations. To ensure type safety, the library verifies that your representation type has the capabilities required for the quantity's character.
Representation Requirements¶
To be used as a representation type in mp-units, a type must satisfy the
RepresentationOf concept. The library supports different
types of representations corresponding to different quantity characters.
Why verify representation capabilities? The same unit can represent fundamentally different physical concepts requiring different mathematical operations. For example:
- speed (scalar, magnitude only) vs. velocity (vector, magnitude and direction) both use m/s,
- mass (scalar) uses kg while weight force (vector, pointing downward) uses N.
The library tracks character in the quantity specification (what the quantity represents) and verifies that your representation type provides the required capabilities (can it handle the operations?). This dual approach provides compile-time type safety for the mathematical nature of physical quantities—preventing, for example, using a scalar type where vector operations like cross product are needed.
The following table summarizes the requirements for different representation characters:
| Requirement | Real Scalar | Complex Scalar | Vector | Tensor |
|---|---|---|---|---|
| Copyable | ✅ | ✅ | ✅ | ✅ |
Addition/subtraction (+, -, unary -) |
✅ | ✅ | ✅ | ✅ |
MagnitudeScalable (unit-conversion) |
✅ | ✅ | ✅ | ✅ |
Self-scalable (T * T, T / T) |
✅ | ✅ | - | - |
Equality comparable (==) |
✅ | ✅ | ✅ | ✅ |
Totally ordered (<, >, <=, >=) |
✅ | - | - | - |
| Not a quantity type itself | ✅ | ✅ | ✅ | ✅ |
| Construction | - | T{real, imag} |
- | - |
| Required CPOs | - | mp_units::real(), mp_units::imag(), mp_units::modulus() |
mp_units::norm() |
mp_units::norm() |
| Opt-out mechanism | disable_real<T> |
- | - | - |
| Examples | int, double, long double |
std::complex<double> |
Eigen::Vector3d, cartesian_vector<double>, int, double |
Eigen::Matrix3d, int, double (for scalar measures) |
Weakly Regular Types
All representation types must be weakly regular, which means they satisfy the
std::regular concept except for the default-constructibility requirement. Specifically,
they must be:
- Copyable (
std::copyable) - Equality comparable (
std::equality_comparable)
This ensures that representation types have value semantics suitable for use in quantities. Default construction is not required, allowing types like range-validated representations that may not have a meaningful default value.
Complex Types Construction and Total Ordering
Construction
Complex scalars must be constructible from real and imaginary parts: T{real_value, imag_value}.
This requirement is essential for operations that combine real-valued quantities into complex results.
For example, combining active power and reactive power into complex power:
quantity active = isq::active_power(100.0 * W);
quantity reactive = isq::reactive_power(50.0 * W);
// Library needs to construct: std::complex<double>{active.numerical_value(), reactive.numerical_value()}
Total Ordering
The library assumes that well-designed complex-like types do not provide total ordering
(operator<, etc.) since there is no natural ordering for complex numbers. If you have
a complex-like type that does provide ordering operators (e.g., lexicographical comparison
for use in containers), use the disable_real opt-out mechanism:
Alternatively, the library could explicitly check for the absence of mp_units::real() and
mp_units::imag() operations to distinguish real from complex scalars. This is a design
choice that may be refined based on standardization discussions.
Why Different CPO Names?
Scalars use modulus(), Vectors and Tensors use norm()
While mathematically related, these represent different domain conventions:
modulus(): Traditional complex analysis terminology for magnitude of complex numbersnorm(): Standard linear algebra terminology used across industry (Eigen, NumPy, MATLAB, Armadillo)
The library follows established domain conventions rather than imposing a single unified terminology.
This makes it more intuitive for users with backgrounds in complex analysis (who expect modulus())
or linear algebra (who expect norm()). Additionally, magnitude() is provided as an alias for
norm() when working with vectors, as "magnitude" is familiar terminology in physics education,
allowing users to choose the name that best fits their domain.
Arithmetic Types Satisfy Multiple Characters
Scalars work as 1D vectors and scalar tensor measures
Arithmetic types like int and double satisfy requirements for:
- Real scalar character (primary use)
- Vector character (representing 1-dimensional vectors)
- Tensor character (representing scalar measures like von Mises stress, principal stress)
This is intentional and extremely common in engineering practice:
// All valid uses of double:
quantity m = isq::mass(5.0 * kg); // Scalar
quantity v = isq::velocity(10.0 * m/s); // 1D vector
quantity sigma = isq::stress(100.0 * Pa); // Scalar tensor measure
The type safety comes from quantity_character matching in the quantity specification,
not from mutually exclusive representation concepts. The RepresentationOf concept ensures
your representation type has the capabilities needed for the quantity's character.
Engineering practice with tensors:
Most engineering doesn't work with full tensor representations (like 3×3 matrices). Instead, scalar measures are extracted and used for analysis:
- Von Mises stress: Single scalar derived from stress tensor (used for failure prediction)
- Principal stresses: Three eigenvalues from stress tensor
- Shear stress components: Individual tensor elements
- Hydrostatic stress: Average of diagonal elements
This is why arithmetic types work for tensor quantities—they represent these scalar measures commonly used in finite element analysis, structural engineering, and materials science.
Customization Points¶
The library provides several customization mechanisms for representation types. For a complete implementation guide and code examples for custom types, see Using Custom Representation Types.
These mechanisms fall into two categories: Character determination (what kind of representation type you have) and Behavior and values (how the library interacts with your type).
Character Determination¶
Customization Point Objects (CPOs)¶
The library uses several CPOs to support different representation types. Providing these CPOs determines the character of your representation type. Each CPO checks for implementations in the following priority order:
mp_units::real(c) - Returns the real part of a complex number:
c.real()member functionreal(c)free function found via ADL
mp_units::imag(c) - Returns the imaginary part of a complex number:
c.imag()member functionimag(c)free function found via ADL
mp_units::modulus(c) - Returns the magnitude of a complex number:
c.modulus()member functionmodulus(c)free function found via ADLc.abs()member functionabs(c)free function found via ADL
mp_units::norm(v) - Returns the norm (magnitude) of a vector or tensor as a scalar:
v.norm()member functionnorm(v)free function found via ADL- For arithmetic types:
std::abs(v) - For real scalar types:
v.abs()member function - For real scalar types:
abs(v)free function found via ADL
mp_units::magnitude(v) - Returns the magnitude of a vector as a scalar:
- Delegates to
mp_units::norm(v)(provided for compatibility with physics terminology)
Why abs() is also checked?
Complex Scalars (modulus() CPO)
Provides compatibility with std::complex and similar types that use abs() as the
function name for returning the modulus value. This is checked as a fallback if
modulus() is not provided.
Vectors and Tensors (norm() CPO)
Allows real scalar types (like int, double) to represent:
- 1-dimensional vectors (very common in engineering for linear motion)
- Scalar tensor measures (von Mises stress, principal stress, etc.)
This enables seamless use of arithmetic types for scalar, vector, AND tensor quantities, which accurately reflects real engineering practice where most calculations use scalar values rather than full vector/tensor representations.
disable_real<T>¶
A specializable variable template to opt out a type from being treated as a real scalar:
Purpose: Controls the character of your representation type by preventing it from being classified as a real scalar, even if it satisfies the real scalar requirements (copyable, totally ordered, supports arithmetic operations).
When to specialize: If your type accidentally satisfies the real scalar requirements but shouldn't be used as one:
Example use case: The library uses this internally to prevent bool from being used
as a scalar representation, even though bool is technically totally ordered and supports
arithmetic operations.
Behavior and Values¶
representation_underlying_type<T>¶
A specializable class template that describes the underlying arithmetic/element type of a representation type. It is the extension point for exposing this information to the library, and is used for:
- Determining the scaling factor type (what type to multiply/divide your type by)
- Checking if the type should be treated as floating-point
template<typename T>
struct mp_units::representation_underlying_type; // primary — empty
template<typename T>
using mp_units::representation_underlying_type_t = representation_underlying_type<T>::type;
Default detection (via partial specializations provided by the library, mirroring
the shape of std::indirectly_readable_traits for its value_type / element_type
cases):
- nested
T::value_type, else - nested
T::element_type; - a top-level
constonTis passed through to the unqualified type; - the detected alias has its cv-qualification removed;
- if
Tprovides bothvalue_typeandelement_typewhose underlying types differ after ignoring top-level cv-qualification, the trait is empty — the user must disambiguate explicitly; - for scoped enumeration types, the underlying integer type is used (via
std::underlying_type_t) — a representation-model extension not present in the standard's iterator-oriented trait. Unscoped enumerations are deliberately excluded because they already implicitly convert to their underlying type.
For your own types: provide a value_type member type so the library detects the
underlying type automatically:
template<typename T>
class my_wrapper {
public:
using value_type = T; // Exposes the underlying type
// ...
};
For third-party types you cannot modify, specialize the trait directly:
// Third-party type MyFloat wraps long double internally
template<>
struct mp_units::representation_underlying_type<MyFloat> {
using type = long double;
};
This makes representation_underlying_type_t<MyFloat> resolve to long double, giving
the library the correct precision for scaling and ensuring the right common_type is
used in mixed conversions.
Don't provide both value_type and element_type
If your type provides both value_type and element_type whose underlying
types differ after ignoring top-level cv-qualification, the trait is empty and
the library treats the type as a leaf. If both are present and name the same
underlying type, that type is used.
Recommendation: Provide only value_type unless you have a specific reason to provide both
(e.g., satisfying iterator concepts), in which case ensure they refer to the same type.
Why not std::indirectly_readable_traits?
std::indirectly_readable_traits answers "what does *t yield?" — it is the
standard's extension point for iterators, smart pointers, and other
indirectly-readable types. Specializing it for a non-iterator representation
type is a semantic misuse: it tells every other standard-library component
that your type is iterator-like, which it usually is not.
Pointer and array specializations from std::indirectly_readable_traits are
intentionally not mirrored in representation_underlying_type — those are
part of the standard's iterator machinery, not of this library's representation
model.
Scaling operators¶
The library scales a representation value by calling value * factor and value / factor,
where factor is of type representation_underlying_type_t<T> (or a wider integer type
for the rational integer path — see widened integers for details).
These operators must be provided so that the built-in scaling paths can apply the unit
magnitude ratio during unit conversions.
Alternatively (or additionally), a type may provide operator*(T, UnitMagnitude) to
receive the full compile-time unit magnitude instead of a numeric factor. When present,
this operator is called first and the underlying-type-based operators serve as a
fallback. The magnitude-aware operator may return a different type — see
Magnitude-aware scaling for the full pattern.
For your own types, provide these as hidden friends (defined inside the class body, found only via ADL):
template<typename T>
class my_wrapper {
T value_;
public:
using value_type = T;
// Hidden friends — preferred over non-member overloads
friend constexpr my_wrapper operator*(my_wrapper v, T factor) { return my_wrapper{v.value_ * factor}; }
friend constexpr my_wrapper operator/(my_wrapper v, T factor) { return my_wrapper{v.value_ / factor}; }
// Optional: magnitude-aware scaling (return type may differ from my_wrapper)
// template<mp_units::UnitMagnitude M>
// friend constexpr auto operator*(const my_wrapper& v, M m) { /* ... */ }
};
For third-party types you cannot modify, place non-member operators in the same namespace as the type so that ADL finds them:
namespace third_party {
// Non-member scaling operators for a type you do not own.
// Must be in the same namespace as the type so ADL finds them.
inline ThirdPartyVec operator*(ThirdPartyVec v, double f) { return v.scale(f); }
inline ThirdPartyVec operator/(ThirdPartyVec v, double f) { return v.scale(1.0 / f); }
} // namespace third_party
See How Scaling Works for the full built-in scaling algorithm, concept definitions, and design rationale.
treat_as_floating_point<Rep>¶
A specializable variable template that tells the library whether a type should be treated as floating-point for the purpose of allowing implicit conversions:
template<typename Rep>
constexpr bool mp_units::treat_as_floating_point = /* implementation-defined */;
Default behavior:
- In hosted environments: uses
std::chrono::treat_as_floating_point_von the (recursively-unwrapped) underlying type ofRep - In freestanding: uses
std::is_floating_point_von the (recursively-unwrapped) underlying type ofRep
When to specialize: If you have a custom type that wraps a floating-point value but the automatic detection doesn't work correctly:
Impact: When treat_as_floating_point<Rep> is true, the type is treated as floating-point
for conversion purposes. See Value Conversions for details
on how this affects implicit conversions between quantities.
implicitly_scalable<FromUnit, FromRep, ToUnit, ToRep>¶
Advanced use case
Most users will never need to specialize implicitly_scalable. The defaults handle
all standard numeric types correctly. Only specialize when you have a custom
representation type with non-standard implicit-conversion semantics.
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. It is the policy layer built on top of
treat_as_floating_point: the default formula derives the implicit-conversion decision
from it, and a specialization overrides that decision for types where the derived rule
is incorrect:
template<auto FromUnit, typename FromRep, auto ToUnit, typename ToRep>
constexpr bool mp_units::implicitly_scalable =
treat_as_floating_point<ToRep> ||
(!treat_as_floating_point<FromRep> && is_integral_scaling(FromUnit, ToUnit));
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 (e.g. m → mm (×1000))
from fractional ones (e.g. mm → m (÷1000), ft → m, deg → rad).
The default rules are:
ToRepis floating-point → always implicit (floating-point absorbs any ratio without truncation)- Both are integer-like and the unit ratio is an integer multiplier → implicit (exact, no information loss)
- Everything else (fractional or irrational ratio with an integer rep) → explicit (truncation possible)
When to specialize: Consider two scenarios where the defaults are wrong for your type:
Scenario 1 — A decimal type that represents fractions exactly
Suppose you have a fixed-point decimal type safe_decimal that can represent any rational
scaling factor (e.g. ×1/1000 for mm→m) without truncation. The default rejects this
implicitly because the ratio is fractional and safe_decimal is not a floating-point type.
You can opt in:
// safe_decimal handles fractional ratios exactly — allow all unit conversions implicitly.
template<auto FromUnit, auto ToUnit>
constexpr bool mp_units::implicitly_scalable<FromUnit, safe_decimal, ToUnit, safe_decimal> =
true;
// Before specialization:
quantity<si::millimetre, safe_decimal> a = safe_decimal{500} * mm;
// quantity<si::metre, safe_decimal> b = a; // ❌ Error without specialization
quantity<si::metre, safe_decimal> b = a; // ✅ Implicit after specialization
Scenario 2 — Asymmetric precision between two types
Suppose my_decimal has higher precision than double, so a double value can always be
represented in my_decimal without loss, but not vice versa:
// double → my_decimal is always lossless: allow it implicitly.
template<auto FromUnit, auto ToUnit>
constexpr bool mp_units::implicitly_scalable<FromUnit, double, ToUnit, my_decimal> = true;
// my_decimal → double may lose precision: keep it explicit (this is also the default,
// shown here for clarity).
template<auto FromUnit, auto ToUnit>
constexpr bool mp_units::implicitly_scalable<FromUnit, my_decimal, ToUnit, double> = false;
// Usage:
quantity<si::metre, double> qd = 1.5 * m;
quantity<si::metre, my_decimal> qdm = qd; // ✅ Implicit (double fits in my_decimal)
quantity<si::metre, my_decimal> qm = my_decimal{1.5} * m;
// quantity<si::metre, double> qdb = qm; // ❌ Error — requires value_cast
quantity<si::metre, double> qdb = value_cast<double>(qm); // ✅ Explicit
You can also reuse the library's own predicate in your specialization:
// Permit implicit conversion only when the unit ratio is an integer multiplier.
// This mirrors the default for integer types but opts my_special_int in explicitly.
template<auto FromUnit, auto ToUnit>
constexpr bool mp_units::implicitly_scalable<FromUnit, my_special_int, ToUnit, my_special_int> =
mp_units::is_integral_scaling(FromUnit, ToUnit);
Impact: Controls whether conversions between quantity types are implicit or require
value_cast/force_in. See Value Conversions
for the full picture.
representation_values<Rep>¶
A specializable class template that provides special values for a representation type:
template<typename Rep>
struct mp_units::representation_values {
static constexpr Rep zero() noexcept; // Required for quantity::zero()
static constexpr Rep one() noexcept; // Required for quantity operations
static constexpr Rep min() noexcept; // Required for quantity::min()
static constexpr Rep max() noexcept; // Required for quantity::max()
};
Default behavior:
- In hosted environments: inherits from
std::chrono::duration_values<Rep> zero(): returnsRep(0)if Rep is constructible fromintone(): returnsRep(1)if Rep is constructible fromintmin(): returnsstd::numeric_limits<Rep>::lowest()if availablemax(): returnsstd::numeric_limits<Rep>::max()if available
When to specialize: If your type needs custom special values:
template<typename T>
struct mp_units::representation_values<my_custom_type<T>> {
static constexpr my_custom_type<T> zero() noexcept
{
return my_custom_type<T>{T{0}};
}
static constexpr my_custom_type<T> one() noexcept
{
return my_custom_type<T>{T{1}};
}
static constexpr my_custom_type<T> min() noexcept
{
return my_custom_type<T>{std::numeric_limits<T>::lowest()};
}
static constexpr my_custom_type<T> max() noexcept
{
return my_custom_type<T>{std::numeric_limits<T>::max()};
}
};
Usage: These values are used by:
quantity::zero(),quantity::min(),quantity::max()static member functions- Mathematical operations like
floor(),ceil(),round() - Division by zero checks
constraint_violation_handler<Rep>¶
A specializable class template that defines how constraint violations are reported for a representation type. For usage and code examples, see Ensure Ultimate Safety.
Character-Specific Operations¶
The library enforces character at compile-time: vector quantities require scalar_product() or
vector_product() instead of *; complex quantities restrict real(), imag(), and modulus()
to quantities of the correct character. See
Character of a Quantity for full
details and examples.
How Scaling Works¶
Every representation type must be unit-conversion scalable — the library must be able
to apply a unit magnitude ratio to it internally. This is captured by the MagnitudeScalable
concept, which directly names the three built-in scaling paths:
concept MagnitudeScalable =
WeaklyRegular<T> && (UsesMagnitudeAwareScaling<T> || UsesFloatingPointScaling<T> || UsesIntegerScaling<T>);
Magnitude-aware scaling
A representation type may additionally (or instead of MagnitudeScalable) provide an
operator*(T, UnitMagnitude) hidden friend. When present, this operator is used first
and the built-in paths act as a fallback. The return type may differ from the input type —
for example, a range-validated representation can return a new type with scaled bounds,
so that a conversion from degrees to radians adjusts the valid range from
[-180, 180] to [-π, π]. See Magnitude-aware scaling for details.
UsesFloatingPointScaling matches any type — or container thereof — whose underlying
type satisfies treat_as_floating_point, is constructible from long double (the
precision at which magnitude constants are evaluated), and supports operator* and
operator/ with that underlying type, returning a weakly-regular result:
concept UsesFloatingPointScaling =
(treat_as_floating_point<T> || treat_as_floating_point<representation_underlying_type_t<T>>) &&
std::constructible_from<representation_underlying_type_t<T>, long double> &&
requires(T value, representation_underlying_type_t<T> f) {
{ value * f } -> WeaklyRegular;
{ value / f } -> WeaklyRegular;
};
UsesIntegerScaling matches any type whose underlying type satisfies detail::integral
(the scaling engine uses get_value<wider_t>, wider_int_for<element_t>, and
fixed_point<element_t> internally, all of which require an integer element type).
Scaling is routed through the type's own operator* and operator/, so wrappers can
check for overflow and containers can scale element-wise. The factor type is
wider_int_for<element_t> — a wider integer of matching sign (e.g. int64_t for
int16_t, uint64_t for uint16_t) — to prevent intermediate overflow in
rational-magnitude conversions:
concept UsesIntegerScaling =
detail::integral<representation_underlying_type_t<T>> &&
requires(T value, wider_int_for<representation_underlying_type_t<T>> wf) {
{ value * wf };
{ value / wf };
};
Why detail::integral and not std::integral?
On GCC in strict mode (-std=c++20), std::integral<__int128> is false because
the standard traits (std::is_integral, std::is_arithmetic) are not specialized
for __int128 outside of GNU extensions (-std=gnu++20). When the platform lacks
__SIZEOF_INT128__ entirely, int128_t and uint128_t are double_width_int<>
software-emulation types that also do not satisfy std::integral.
detail::integral patches both gaps:
template<typename T>
concept detail::integral =
std::integral<T> ||
std::same_as<std::remove_cv_t<T>, int128_t> ||
std::same_as<std::remove_cv_t<T>, uint128_t>;
The scaling engine internals (get_value, wider_int_for, fixed_point) are all
specialized for int128_t / uint128_t, so the full integer scaling pipeline
works correctly for 128-bit element types on all supported compilers.
Most standard types satisfy MagnitudeScalable automatically. See
Scaling operators in the Customization Points section for how to
provide operator* and operator/ for your own types.
Built-in scaling algorithm¶
When two quantities of convertible units are combined or converted, the library applies the
unit magnitude M to the representation value via scale<To>(M, value). The
built-in decision tree is:
flowchart TD
A["scale(M, value)"] --> MA{"provides<br>op*(T, UnitMagnitude)?"}
MA -- "Yes" --> MAR["<b>Magnitude-aware scaling</b><br>calls value * M{}<br>return type may differ from input<br><br>e.g. custom type<br>with scaled bounds"]
MA -- "No (fallback)" --> B{"treat_as_floating_point<T><br>or treat_as_floating_point<representation_underlying_type_t<T>><br>?"}
B -- True --> FP["<b>UsesFloatingPointScaling</b><br>ratio at source's representation_underlying_type_t precision<br><br>e.g. double, cartesian_vector<double>"]
B -- False --> INT["<b>UsesIntegerScaling</b><br>e.g. int, safe_int<int>, cartesian_vector<int>"]
INT --> G{"magnitude?"}
G -- "integral (e.g. m→mm, ×1000)" --> I["exact integer multiplication"]
G -- "rational (e.g. ft→m, ×3048/10000)" --> R["widened integer arithmetic<br>(int64_t/uint64_t or 128-bit;<br>signedness-preserving;<br>avoids overflow & FP rounding)"]
G -- "irrational (e.g. deg→rad, ×π/180)" --> IR["long double fixed-point approximation"]
The magnitude-aware path (operator*(T, UnitMagnitude)) is checked first—before any of
the built-in paths. If a representation type provides this operator, it has full control
over how scaling is performed and what type is returned. The built-in paths are only used
as a fallback when this operator is not available.
The integer path (UsesIntegerScaling) never promotes
values to floating-point, even for the rational and irrational sub-paths. This is
intentional: the user explicitly chose an integer representation type, opting out of
floating-point arithmetic. Their platform may lack FP hardware (embedded systems, DSPs),
rely on software-emulated FP (slow and unpredictable), or enforce a no-FP policy. The
library respects that choice throughout unit conversion.
Why fixed-point arithmetic for integer representations?
-
Lossless conversions must stay exact
42 * mconverted tommmust give exactly42000 * mm. Integer multiplication achieves this; going viadoublewould produce correct results for small values but lose exactness near the precision limit (53-bit mantissa ≈ 9×10¹⁵). -
Rational factors remain exactly representable
The library multiplies by the numerator, then divides by the denominator using integer arithmetic. The result is exact whenever the integer is divisible by the denominator. Requiring an explicit cast signals that truncation might occur.
-
Irrational factors are an unavoidable last resort
π/180 (deg→rad), √2, etc. cannot be represented exactly in any integer arithmetic. The library falls back to a
long doubleapproximation and rounds the result to the target integer type ("fixed-point approximation").
The design preference order is therefore: exact integer > exact rational > approximate irrational.
Why widened integers for the rational path?
The library computes value * numerator / denominator entirely in integer
arithmetic. Without extra width, the intermediate product
value * numerator can overflow even when the final result fits — for example,
converting feet to metres multiplies by 3048 before dividing by 10000, which
overflows a 64-bit integer for values above ~3×10¹⁵.
mp-units uses widened integers to absorb that intermediate growth:
- For signed types up to 32 bits (
int8_t,int16_t,int32_t): widens toint64_t - For unsigned types up to 32 bits (
uint8_t,uint16_t,uint32_t): widens touint64_t - For
int64_t: widens to signed 128-bit arithmetic (__int128or custom emulation) - For
uint64_t: widens to unsigned 128-bit arithmetic (unsigned __int128or custom emulation)
This approach provides maximum safety headroom for smaller types (with no performance
cost on modern 64-bit systems), while 128-bit arithmetic handles the vast majority
of real-world int64_t conversions. Using long double instead would violate the
no-FP principle above and introduce rounding (0.3048 is not exactly representable
in binary floating-point), and on ARM / Apple Silicon long double == double anyway,
giving no extra range.
Detecting overflow in the final result
While double-width arithmetic avoids UB during the intermediate scaling multiplication,
it cannot prevent overflow in the final result when that result doesn't fit in the
target type. For runtime overflow detection, use
safe_int<T>, which wraps the representation type and checks all
arithmetic operations for overflow.
If different internal fields need different scale factors, encode that logic in
operator* and operator/ — the library routes scaling through them via the
appropriate built-in path. For an example of integrating a third-party floating-point
type, see the
MyFloat example
in the how-to guide.
Magnitude-aware scaling¶
Some representation types need to transform not just their value but also their type during unit conversion. For example, a range-validated representation that constrains values to [-180, 180] (degrees) should produce a type constrained to [-π, π] when converted to radians — otherwise the bounds would be meaningless or overly restrictive in the target unit.
To support this, provide an operator*(T, UnitMagnitude) hidden friend. It receives the
exact compile-time unit magnitude and can return a different type (e.g. the same
template with different bounds):
// Example custom type (not provided by the library)
template<mp_units::treat_as_floating_point T, auto Min, auto Max, typename Policy>
class bounded_value : /* ... */ {
public:
template<mp_units::UnitMagnitude M>
[[nodiscard]] friend constexpr auto operator*(const bounded_value& val, M m)
{
constexpr T new_lo = mp_units::scale<T>(M{}, T{Min});
constexpr T new_hi = mp_units::scale<T>(M{}, T{Max});
const T scaled = mp_units::scale<T>(m, val.value());
if constexpr (new_lo <= new_hi)
return bounded_value<T, new_lo, new_hi, Policy>(scaled);
else
return bounded_value<T, new_hi, new_lo, Policy>(scaled);
}
};
The scale function handles precision optimization automatically — when the
magnitude's inverse is integral (e.g. degree-to-radian with π/180), it divides by the
inverse instead of multiplying, avoiding FP rounding errors.
The library calls value * M{} in scale() before trying the built-in paths.
Because the return type may differ from the input, quantity::in(unit) propagates
the new representation type through sudo_cast, and the resulting quantity (or
quantity_point) automatically uses the scaled-bounds representation.
Bounded Quantity Points
For bounded quantity_point types, the library provides a different mechanism:
overflow policies can be passed directly as template parameters to point origins.
See Range-Validated Quantity Points.
Built-in Support¶
All fundamental arithmetic types except bool satisfy the real scalar and 1-D vector
representation requirements:
quantity<si::metre, int> length1 = 42 * m;
quantity<si::metre, double> length2 = 3.14 * m;
quantity<si::metre, long double> length3 = 2.718L * m;
Why is bool excluded?
Although bool technically satisfies the syntactic requirements (it's copyable, totally
ordered, and supports arithmetic operations), it's excluded via disable_real<bool> = true
because using boolean values as physical quantities rarely makes sense and can lead to
confusing code.
The library also supports std::complex for complex-valued quantities:
#include <complex>
std::complex<double> impedance{50.0, 30.0};
quantity z = impedance * si::ohm; // Complex impedance
Beyond these built-in types, any custom type works as a representation as long as it
satisfies the RepresentationOf concept for the desired
character. At minimum this means:
- providing
value_type(orelement_type) so the library knows the underlying scalar type, - providing
operator*andoperator/withrepresentation_underlying_type_t<T>so the library can scale it during unit conversions (and optionallyoperator*(T, UnitMagnitude)for magnitude-aware scaling), - satisfying the character-specific requirements from the table above (copyable, equality comparable, arithmetic operators, CPOs, etc.).
See Using Custom Representation Types for a step-by-step walkthrough, including the complete set of customization points and working examples.
See Also¶
- Character of a Quantity - Understanding quantity characters
- Value Conversions - How
treat_as_floating_pointaffects conversions - Concepts - The
RepresentationOfconcept definition - Using Custom Representation Types - Step-by-step guide to creating custom representation types
- Tutorial: Custom Contract Handlers - Implementing custom error policies for
constrainedtypes - Ensure Ultimate Safety - Combining
constrainedreps with bounded quantity points