Skip to content

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, and bool are 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:

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()}
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 types
  • magnitude(): 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:

  1. c.real() member function
  2. real(c) free function found via ADL

mp_units::imag(c) - Returns the imaginary part of a complex number:

  1. c.imag() member function
  2. imag(c) free function found via ADL

mp_units::modulus(c) - Returns the magnitude of a complex number:

  1. c.modulus() member function
  2. modulus(c) free function found via ADL
  3. c.abs() member function
  4. abs(c) free function found via ADL

mp_units::magnitude(v) - Returns the magnitude (norm) of a vector or tensor as a scalar:

  1. v.magnitude() member function
  2. magnitude(v) free function found via ADL
  3. For non-scalar types: v.norm() member function
  4. For non-scalar types: norm(v) free function found via ADL
  5. For arithmetic types: std::abs(v)
  6. For complex scalar types: modulus(v)
  7. For real scalar types: v.abs() member function
  8. 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 , 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:

template<>
constexpr bool mp_units::disable_representation<my_type> = true;

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 to quantity_field::real.
  • A complex-element vector or tensor that cannot expose real()/imag() on its surface but is nonetheless usable as complex. Specialize to quantity_field::complex.
template<>
constexpr quantity_field mp_units::numeric_field<my_real> = quantity_field::real;

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 const on T is passed through to the unqualified type;
  • the detected alias has its cv-qualification removed;
  • if T provides both value_type and element_type whose 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_v on the (recursively-unwrapped) underlying type of Rep
  • In freestanding: uses std::is_floating_point_v on the (recursively-unwrapped) underlying type of Rep

When to specialize: If you have a custom type that wraps a floating-point value but the automatic detection doesn't work correctly:

template<>
constexpr bool mp_units::treat_as_floating_point<my_fixed_point_type> = true;

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:

  • ToRep is 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(): returns Rep(0) if Rep is constructible from int
  • one(): returns Rep(1) if Rep is constructible from int
  • min(): returns std::numeric_limits<Rep>::lowest() if available
  • max(): returns std::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&lt;T&gt;<br>or treat_as_floating_point&lt;representation_underlying_type_t&lt;T&gt;&gt;<br>?"}
    B -- True --> FP["<b>UsesFloatingPointScaling</b><br>ratio at source's representation_underlying_type_t precision<br><br>e.g. double, cartesian_vector&lt;double&gt;"]
    B -- False --> INT["<b>UsesIntegerScaling</b><br>e.g. int, safe_int&lt;int&gt;, cartesian_vector&lt;int&gt;"]
    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 &amp; 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?
  1. Lossless conversions must stay exact

    42 * m converted to mm must give exactly 42000 * mm. Integer multiplication achieves this; going via double would produce correct results for small values but lose exactness near the precision limit (53-bit mantissa ≈ 9×10¹⁵).

  2. 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.

  3. 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 double approximation 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 to int64_t
  • For unsigned types up to 32 bits (uint8_t, uint16_t, uint32_t): widens to uint64_t
  • For int64_t: widens to signed 128-bit arithmetic (__int128 or custom emulation)
  • For uint64_t: widens to unsigned 128-bit arithmetic (unsigned __int128 or 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 (or element_type) so the library knows the underlying scalar type,
  • providing operator* and operator/ with representation_underlying_type_t<T> so the library can scale it during unit conversions (and optionally operator*(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