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 requirements split along the two independent axes of a quantity's character: the field (real or complex) and the order (scalar, vector, or tensor). A representation type must satisfy a common baseline, then the requirements of its field and of its order.
Common baseline (every representation type)¶
Independent of character, a representation type must be:
- copyable (
std::copyable) and equality comparable (==), - support addition and subtraction (
+,-, unary-), UnitMagnitudeScalable, so the library can apply a unit magnitude ratio to it during unit conversion,- not opted out through
disable_representation<T>(a quantity or quantity-like type, a container of either, andboolare opted out by default).
On top of that baseline, the type's two character axes each add their own requirements. The
field is reported by numeric_field<T> and the order by
tensor_order<T>.
Order axis — tensor_order<T>¶
The orders are ranked: a lower-order representation also fills a higher-order slot, so
a scalar backs a vector or tensor quantity and a vector backs a tensor quantity. Override
the detected order by specializing tensor_order<T>.
| Requirement | Scalar (0) | Vector (1) | Tensor (2) |
|---|---|---|---|
Element access (drives default tensor_order detection) |
- | t[i] |
t(i, j) or t[i, j] |
Self-scalable (T * T, T / T) |
✅ | - | - |
mp_units::magnitude() CPO (norm() fallback) |
- | ✅ | ✅ |
| Examples | int, double, std::complex<double> |
cartesian_vector, Eigen::Vector3d |
cartesian_tensor, Eigen::Matrix3d |
Field axis — numeric_field<T>¶
The field is matched exactly: a real quantity needs a real representation and a complex one
a complex representation. It is reported by numeric_field<T>, which reads
the field off a scalar element of the representation (reached by indexing for a vector or
tensor, or the type itself for a scalar), so a container is complex exactly when its element
is. The requirements below are what a scalar representation provides for each field. In
the rare case a type misreports its element's field, override it by specializing
numeric_field<T>.
| Requirement (on a scalar representation) | Real | Complex |
|---|---|---|
Totally ordered (<, >, <=, >=) |
✅ | - |
Construction from parts (T{real, imag}) |
- | ✅ |
mp_units::real(), mp_units::imag(), mp_units::modulus() CPOs |
- | ✅ |
| Examples | int, double, cartesian_vector<double> |
std::complex<double>, cartesian_vector<std::complex<double>> |
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.
Constructing Complex Scalars
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:
Why Different CPO Names?
Complex scalars use modulus(), Vectors and Tensors use magnitude()
While mathematically related, these represent different domain conventions:
modulus(): Traditional complex analysis terminology for the magnitude of a complex number; the library CPO for complex scalar typesmagnitude(): Physics education terminology for the magnitude of a vector; the library CPO for vector types
To integrate zero-friction with linear algebra libraries (Eigen, NumPy, MATLAB, Armadillo) that
conventionally name this operation norm(), the magnitude() CPO also recognizes norm() as a
fallback for genuine vector types (i.e. types that are neither real nor complex scalars). This means
existing types with a norm() member or free function work without any adaptation.
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.
The same rank-ordering applies to a complex scalar (such as std::complex): it is a valid
1-dimensional complex vector, and its magnitude is the modulus |z| of its single component,
symmetric with double standing in as a real 1-D vector above.
Engineering practice with tensors:
For the full second-order tensor, the library provides the built-in cartesian_tensor (a fixed
3×3 representation). Most engineering, however, does not work with full 3×3 matrices. Instead,
scalar measures are extracted from the tensor field 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 remain useful for tensor quantities. They represent these scalar
measures commonly used in finite element analysis, structural engineering, and materials science,
without paying for a full matrix. The rank-ordering also lets a cartesian_vector stand in for a
tensor quantity, since a vector is a tensor of the first order.
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).
Note
Every customization point in this section is keyed on the representation type. One further
customization point, vector_components, is keyed on the quantity spec instead: it opts a
vector quantity into decomposition into named components.
Because it parameterizes on the quantity rather than its representation, it is documented in
its own section below rather than listed here.
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::magnitude(v) - Returns the magnitude (norm) of a vector or tensor as a scalar:
v.magnitude()member functionmagnitude(v)free function found via ADL- For non-scalar types:
v.norm()member function - For non-scalar types:
norm(v)free function found via ADL - For arithmetic types:
std::abs(v) - For complex scalar types:
modulus(v) - For real scalar types:
v.abs()member function - For real scalar types:
abs(v)free function found via ADL
Why norm() and abs() are also checked?
Vectors and Tensors: norm() fallback
Linear algebra libraries (Eigen, NumPy, MATLAB, Armadillo) conventionally name the
Euclidean norm norm(). Recognizing norm() as a fallback (steps 3–4) means existing
vector types integrate without any adaptation. The fallback is guarded to non-scalar
types (neither real nor complex scalars) for two reasons: std::norm(double) (from
<complex>) returns x², not |x|; and std::norm(complex) returns |z|², not
|z|. Both would produce silently wrong magnitudes if norm() were called on scalar
types. If norm were ever promoted to a proper CPO, this guarding would make its
semantics unambiguous.
Vectors and Tensors: abs() fallback for arithmetic types
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.
Complex scalars as 1-D complex vectors: modulus() (step 6)
Symmetrically, a complex scalar (such as std::complex) stands in for a 1-dimensional
complex vector, whose magnitude is the modulus |z| of its single component. So the
magnitude() CPO routes a complex scalar through modulus(), mirroring the arithmetic
abs() case above.
Complex Scalars: abs() fallback
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.
disable_representation<T>¶
A specializable variable template to opt a type out of being a quantity representation, regardless of character:
template<typename T>
constexpr bool mp_units::disable_representation = /* true if T is, or its elements are, a quantity or quantity-like type */;
Purpose: Bars T from satisfying any of the representation concepts, even when it
structurally looks like a valid scalar, vector, or tensor. This is the single,
character-agnostic escape hatch: the field and order are answered by the numeric_field and
tensor_order traits, and this trait answers the orthogonal question "should T be a
representation at all?".
Default: A type is opted out when it is, or its elements are, a quantity or a
quantity-like type. So a bare quantity, a std::chrono::duration,
and a container of either are all rejected. The library also opts out bool,
which is totally ordered and supports arithmetic yet is not a meaningful representation.
When to specialize: When a type accidentally satisfies a representation concept but must never store a quantity:
numeric_field<T>¶
A specializable variable template that reports the field of a representation type, real or complex. It is the single source of truth for the field axis:
template<typename T>
constexpr quantity_field mp_units::numeric_field =
/* field of a scalar element of T: complex if that element satisfies the
mp_units::real()/imag() CPOs, real otherwise; for a vector or tensor a complex
element additionally requires the container itself to expose real()/imag() */;
Default: The field is read off a scalar element of the representation. For an
order >= 1 type (a vector or tensor) the trait recurses into one element reached by indexing;
for a scalar (order 0) it checks the mp_units::real() and mp_units::imag() CPOs directly.
So std::complex<double> is complex and double real.
A vector or tensor carries the field of its element, plus one consistency requirement:
a container whose element is complex must also expose the real()/imag() decomposition
API on its own surface. A cartesian_vector<std::complex<double>> is complex (it exposes
real()/imag()) and a cartesian_vector<double> is real, with no extra wiring.
This handles the linear algebra libraries for free from both sides. Eigen and Blaze expose
real() and imag() on their real matrices and vectors (a real value is a degenerate
complex one), which would fool a check made only on the container surface, but the field is
read from the (real) element, so a real Eigen matrix is correctly real. Their complex
matrices carry a complex element and expose real()/imag(), so they are correctly
complex.
A container with a complex element that does not expose real()/imag() cannot be used
as a complex representation (the decomposition API is missing). Rather than silently classify
it real, which would pick a Euclidean instead of a Hermitian magnitude, its field is left
undefined here, exactly as tensor_order is left undefined for an ambiguous shape. The
field and order axes then reject such a type together until the author either exposes the
API or specializes numeric_field.
When to specialize: Rarely, in one of two cases:
- A scalar type that exposes
real()/imag()yet is meant to be real (a "degenerate complex" that supports generic call sites). Specialize toquantity_field::real. - A complex-element vector or tensor that cannot expose
real()/imag()on its surface but is nonetheless usable as complex. Specialize toquantity_field::complex.
Field matching is exact: a real quantity requires a real representation and a complex
quantity a complex one. A double does not satisfy a complex slot, and a
std::complex<double> does not satisfy a real one. This prevents silently dropping the
imaginary part when a complex value is assigned where a real one is expected.
tensor_order<T>¶
A specializable variable template that reports the intrinsic order of a representation
type: 0 for a scalar, 1 for a vector, 2 for a second-order tensor. Its primary template
is left undefined. A specialization detects the order for a type that exposes exactly one
indexing shape:
template<typename T>
requires /* T exposes exactly one indexing shape */
constexpr std::size_t mp_units::tensor_order<T> = /* t(i, j) -> 2, t[i] -> 1, otherwise 0 */;
Default: The order is detected structurally from the type's element access: two-index
access (the call operator t(i, j) or the C++23 multidimensional subscript t[i, j]) is
order 2, single-index access (t[i]) is order 1, and anything else is order 0. A type
that exposes both shapes is ambiguous — an N×1 matrix models a vector yet also offers
t(i, j), and only its compile-time extents can decide — so it has no default and must
be specialized. This is the single source of truth for the order: numeric_field consults
it too rather than guessing separately.
When to specialize: For an ambiguous type, whose order the library will not guess. The
prominent example is Eigen: its column vector is an N×1 matrix exposing both t[i] and
t(i, j), so its adapter declares the order from Eigen's compile-time shape:
template<typename T>
requires /* T is an Eigen type */
constexpr std::size_t mp_units::tensor_order<T> =
(T::RowsAtCompileTime == 1 || T::ColsAtCompileTime == 1) ? 1 : 2;
An ordinary full specialization works too (template<> constexpr std::size_t
mp_units::tensor_order<my_type> = 2;). Until specified, an ambiguous type is simply not a
valid representation — a SFINAE-friendly rejection, so forgetting the adapter is a clean
compile error at the point of use, not a silent misclassification.
Order matching is rank-ordered: a representation fills a slot of equal or higher order.
A scalar can back a vector or a tensor quantity (very common in engineering, where double
models a one-dimensional vector or a scalar tensor measure), and a cartesian_vector can
back a tensor quantity. The reverse never holds, so a cartesian_tensor (order 2) does
not satisfy a vector or scalar quantity.
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.
representation_canonical_type<T>¶
A specializable class template that maps a representation value type to the
concrete type a quantity should store. The primary template simply decays the type:
template<typename T>
struct mp_units::representation_canonical_type {
using type = std::remove_cvref_t<T>;
};
template<typename T>
using mp_units::representation_canonical_type_t = /* ... decays a top-level const ... */;
Why it exists: expression-template linear algebra libraries (e.g. Eigen, Blaze)
return lazy proxy types from their arithmetic operators. Such a proxy keeps references
to its operands and must be evaluated to a concrete type before being stored inside a
quantity; otherwise the quantity would retain dangling references once the operands
(often temporaries) go out of scope. The quantity deduction guides and the concepts
that compute the representation type resulting from an arithmetic operation consult this
trait, so the stored representation is always a materialized concrete type.
When to specialize: to teach the library how to evaluate a specific
expression-template type. The ready-made
third-party integration plugins do this for you
(e.g. mapping Eigen's PlainObject and Blaze's ResultType). For a type whose operators
already return a concrete value (arithmetic types, cartesian_vector, GLM) the default
is correct and no specialization is needed.
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 unit-magnitude-aware operator may return a different type — see
Unit-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: unit-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.
Decomposing a Vector Quantity¶
A vector quantity can be split into named, strongly-typed 1D-vector component quantities,
reachable by index (get<Idx>), by quantity spec (get<QS>), and through structured bindings.
The whole opts in by specializing the vector_components customization point.
Decomposition adds one requirement on top of the vector representation requirements above:
the representation must be indexable by a compile-time constant index, through either
a tuple-like get<Idx>(rep) (found by argument-dependent lookup) or a subscript
rep[Idx]. The built-in cartesian_vector and the supported third-party vectors all
qualify. If the representation also models std::tuple_size, the library checks at
compile time that the declared axis count does not exceed what the representation holds.
See Decompose a Vector Quantity into Components for the full recipe, including how the quantity hierarchy must be formed.
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 UnitMagnitudeScalable
concept, which directly names the three built-in scaling paths:
concept UnitMagnitudeScalable =
WeaklyRegular<T> && (UsesUnitMagnitudeAwareScaling<T> || UsesFloatingPointScaling<T> || UsesIntegerScaling<T>);
Unit-magnitude-aware scaling
A representation type may additionally (or instead of UnitMagnitudeScalable) 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 Unit-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 UnitMagnitudeScalable 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>Unit-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 unit-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.
Unit-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 is copyable, totally
ordered, and supports arithmetic operations), it is excluded via
disable_representation<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
For vector and tensor quantities, the library ships two built-in representation types:
cartesian_vector<T, N> (an N-dimensional Cartesian vector) and cartesian_tensor<T, N>
(an N×N second-order Cartesian tensor). Both take a compile-time dimension N,
either 2 or 3 and defaulting to 3, so a planar model uses a 2-D vector or a 2×2
tensor without paying for an unused third component. Each provides the ISO 80000-2
operations for its character.
Both types live in the mp_units::utility namespace. The snippets below assume a
using namespace mp_units::utility;.
#include <mp-units/utility/cartesian_tensor.h> // also pulls in cartesian_vector.h
quantity v = cartesian_vector{1., 2., 3.} * isq::velocity[m / s];
quantity sigma = cartesian_tensor{1., 0., 0., 0., 1., 0., 0., 0., 1.} * isq::stress[Pa];
The dimension is deduced from the number of components, and operations close only at a
matching N. embed and project convert explicitly between the plane and space,
zero-filling or dropping the last coordinate:
cartesian_vector v2{1., 2.}; // a 2-D vector, N deduced as 2
quantity planar = isq::velocity(v2 * m / s); // usable as a 2-D vector quantity
cartesian_vector v3 = embed(v2); // lift to 3-D: (x, y) -> (x, y, 0)
quantity spatial = isq::velocity(v3 * m / s); // a 3-D velocity quantity
cartesian_vector back = project(v3); // drop to 2-D: (x, y, z) -> (x, y); back == v2
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 unit-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.
Third-Party Library Integrations¶
You usually do not need to wire up the customization points described above yourself for
a mainstream third-party library. mp-units ships opt-in integration plugins — currently
for Eigen, GLM, and
Blaze — so their vector and matrix types can be
used directly as vector/tensor representations. The
Using a Linear Algebra Library
how-to guide is the single home for the shipped-plugin list, the recipe for adapting a
library that has no plugin, and the caveats (expression templates, the unsupported Armadillo
operator==, and the V2 vector-operation result-type limitation).
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