Using Custom Representation Types¶
This guide shows you how to create and integrate your own custom representation types with mp-units. You'll learn the steps needed to make your type work seamlessly with the library's quantity system.
For background on representation type design and requirements, see the Representation Types section in the User's Guide.
Quick Start¶
Creating a quantity with a custom representation type is straightforward:
#include <mp-units/systems/si.h>
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
my_custom_type value{42};
auto distance = value * m; // quantity<si::metre, my_custom_type>
Your custom type must satisfy the library's
RepresentationOf concept,
which verifies your type provides the operations needed for its intended
quantity character.
Creating Your Own Representation Type¶
Follow these steps to create a custom representation type that works with mp-units.
Step 1: Define Your Type with Required Operations¶
Create a class with value semantics and the operations your character needs. Here's a template for a real scalar type:
template<typename T>
class my_scalar_type {
T value_;
public:
using value_type = T; // Helps library determine scaling factor type
constexpr explicit my_scalar_type(T v) : value_(v) {}
[[nodiscard]] constexpr T value() const { return value_; }
// Required: Arithmetic operations
[[nodiscard]] constexpr my_scalar_type operator-() const { return my_scalar_type{-value_}; }
[[nodiscard]] friend constexpr my_scalar_type operator+(const my_scalar_type& lhs, const my_scalar_type& rhs)
{
return my_scalar_type{lhs.value_ + rhs.value_};
}
[[nodiscard]] friend constexpr my_scalar_type operator-(const my_scalar_type& lhs, const my_scalar_type& rhs)
{
return my_scalar_type{lhs.value_ - rhs.value_};
}
[[nodiscard]] friend constexpr my_scalar_type operator*(const my_scalar_type& v, T factor)
{
return my_scalar_type{v.value_ * factor};
}
[[nodiscard]] friend constexpr my_scalar_type operator*(T factor, const my_scalar_type& v)
{
return my_scalar_type{factor * v.value_};
}
[[nodiscard]] friend constexpr my_scalar_type operator/(const my_scalar_type& v, T factor)
{
return my_scalar_type{v.value_ / factor};
}
// Required for scalar types: Self-scaling operations (multiply/divide by same type)
[[nodiscard]] friend constexpr my_scalar_type operator*(const my_scalar_type& lhs, const my_scalar_type& rhs)
{
return my_scalar_type{lhs.value_ * rhs.value_};
}
[[nodiscard]] friend constexpr my_scalar_type operator/(const my_scalar_type& lhs, const my_scalar_type& rhs)
{
return my_scalar_type{lhs.value_ / rhs.value_};
}
// Required for real scalar types: Equality & Total ordering
[[nodiscard]] constexpr auto operator<=>(const my_scalar_type&) const = default;
};
Step 2: Provide Character-Specific Customization Points (if needed)¶
CPOs vs Customization Point Functions
The library provides Customization Point Objects (CPOs) like mp_units::real, mp_units::imag,
mp_units::norm, etc. You provide customization point functions (as member functions or
ADL-findable free functions) that these CPOs will find and invoke.
For complex scalars, provide the required customization point functions via member functions:
template<typename T>
class my_complex_type {
T real_, imag_;
public:
using value_type = T;
constexpr my_complex_type(T r, T i) : real_(r), imag_(i) {}
// Required customization point functions as member functions
[[nodiscard]] constexpr T real() const { return real_; }
[[nodiscard]] constexpr T imag() const { return imag_; }
[[nodiscard]] constexpr T modulus() const { return std::hypot(real_, imag_); }
// ... other required operations (addition, scaling, equality)
};
Or via free functions found through ADL:
template<typename T>
[[nodiscard]] constexpr T real(const my_complex_type<T>& c) { return c.get_real(); }
template<typename T>
[[nodiscard]] constexpr T imag(const my_complex_type<T>& c) { return c.get_imag(); }
template<typename T>
[[nodiscard]] constexpr T modulus(const my_complex_type<T>& c) { return c.get_magnitude(); }
For vectors, provide the norm() customization point function:
template<typename T>
class my_vector_type {
// ... implementation
public:
using value_type = T;
constexpr auto norm() const { /* compute magnitude */ }
// ... other required operations
};
Or via a free function:
template<typename T>
[[nodiscard]] constexpr auto norm(const my_vector_type<T>& v) { return v.compute_norm(); }
Use norm() for vectors
While magnitude() is also supported for compatibility, prefer implementing norm() to match
industry standard libraries (Eigen, NumPy, MATLAB, Armadillo).
Step 3: Add Formatting Support (optional)¶
Enable formatting with std::format:
template<typename T, typename Char>
struct std::formatter<my_scalar_type<T>, Char> : std::formatter<T, Char> {
template<typename FormatContext>
auto format(const my_scalar_type<T>& v, FormatContext& ctx) const
{
return std::formatter<T, Char>::format(v.value(), ctx);
}
};
Step 4: Specialize representation_values (if needed)¶
If your type needs custom special values (see the
representation_values<Rep>
documentation):
template<typename T>
struct mp_units::representation_values<my_scalar_type<T>> {
[[nodiscard]] static constexpr my_scalar_type<T> zero() noexcept { return my_scalar_type<T>{T{0}}; }
[[nodiscard]] static constexpr my_scalar_type<T> one() noexcept { return my_scalar_type<T>{T{1}}; }
[[nodiscard]] static constexpr my_scalar_type<T> min() noexcept
{
return my_scalar_type<T>{std::numeric_limits<T>::lowest()};
}
[[nodiscard]] static constexpr my_scalar_type<T> max() noexcept
{
return my_scalar_type<T>{std::numeric_limits<T>::max()};
}
};
If your type will be used with bounds checking or other constraint enforcement mechanisms,
you may also want to specialize constraint_violation_handler to control error reporting:
template<typename T>
struct mp_units::constraint_violation_handler<my_scalar_type<T>> {
static void on_violation(std::string_view msg) {
// Custom error handling: throw, log, abort, etc.
throw std::runtime_error(std::string(msg));
}
};
See the Ensure Ultimate Safety guide for details on using bounds checking with quantity points.
Step 5: Enable scaling¶
The library applies a unit magnitude to a representation value internally when performing unit conversions. Three built-in paths handle this automatically — see How Scaling Works for the full concept definitions and algorithm.
my_scalar_type<T> already satisfies the floating-point or integer scaling path
(UsesFloatingPointScaling or UsesIntegerScaling) through its existing
operator*(my_scalar_type, T) and operator/(my_scalar_type, T) —
no further customization is needed.
If your type is not automatically recognized (e.g., a third-party floating-point type with
no value_type member), expose the underlying type via
representation_underlying_type —
treat_as_floating_point will then default to true automatically, with no further
specialization needed. The example below shows this typical case.
Only specialize
treat_as_floating_point
directly when there is genuinely no meaningful underlying type to expose.
Magnitude-aware scaling (optional)¶
If your representation type needs to transform its type during unit
conversion — not just its value — provide an operator*(T, UnitMagnitude) hidden friend.
This is checked before the built-in paths and may return a different type.
A typical use case is a range-validated representation whose bounds are unit-specific:
#include <mp-units/framework/unit_magnitude.h>
// Example custom type (not provided by the library)
template<std::movable T, auto Min, auto Max, typename Policy>
class bounded_value {
// ...
public:
template<mp_units::UnitMagnitude M>
requires mp_units::treat_as_floating_point<T>
[[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); // different type!
else
return bounded_value<T, new_hi, new_lo, Policy>(scaled);
}
};
See Magnitude-aware scaling in the User's Guide for the complete pattern and design rationale.
MyFloat — integrating a third-party floating-point type
Suppose a third-party library provides a high-precision floating-point type that you cannot modify:
// Third-party type — you cannot modify the source.
class MyFloat {
long double v_;
public:
explicit(false) MyFloat(long double v) : v_(v) {}
MyFloat operator-() const { return MyFloat{-v_}; }
friend MyFloat operator+(MyFloat a, MyFloat b) { return MyFloat{a.v_ + b.v_}; }
friend MyFloat operator-(MyFloat a, MyFloat b) { return MyFloat{a.v_ - b.v_}; }
friend MyFloat operator*(MyFloat a, MyFloat b) { return MyFloat{a.v_ * b.v_}; }
friend MyFloat operator/(MyFloat a, MyFloat b) { return MyFloat{a.v_ / b.v_}; }
friend auto operator<=>(MyFloat, MyFloat) = default;
};
MyFloat is floating-point in spirit but the library cannot detect this automatically:
- It has no
value_typemember, sorepresentation_underlying_type<MyFloat>is empty andMyFloatis treated as a leaf (its own underlying type). treat_as_floating_point<MyFloat>defaults tostd::is_floating_point_v<MyFloat>=false(it is a class, not a fundamental type), soMagnitudeScalable<MyFloat>is not satisfied.
One specialization fixes this:
// Expose the underlying type so representation_underlying_type_t<MyFloat> == long double.
// treat_as_floating_point<MyFloat> then defaults to
// std::is_floating_point_v<long double> == true, so no further
// specialization is needed.
template<>
struct mp_units::representation_underlying_type<MyFloat> {
using type = long double;
};
After that specialization MyFloat satisfies UsesFloatingPointScaling and
integrates with the library without any further changes:
Step 6: Specialize implicitly_scalable (if needed)¶
By default, a conversion between two quantity types is implicit only when it is
non-truncating: the target representation is floating-point, or both representations are
integer-like and the unit ratio is an integer multiplier (e.g. m → mm). All other
integer-to-integer conversions (fractional ratios such as mm → m) are explicit and
require value_cast or force_in.
If your type has different implicit-conversion semantics, specialize
mp_units::implicitly_scalable:
// Allow implicit narrowing from double to my_safe_decimal (it can represent any double value)
template<auto FromUnit, auto ToUnit>
constexpr bool mp_units::implicitly_scalable<FromUnit, double, ToUnit, my_safe_decimal> = true;
// Keep double → my_safe_decimal explicit (my_safe_decimal has higher precision)
template<auto FromUnit, auto ToUnit>
constexpr bool mp_units::implicitly_scalable<FromUnit, my_safe_decimal, ToUnit, double> = false;
You can use mp_units::is_integral_scaling(from, to) in your specialization to reuse the
library's "is the ratio an integer?" predicate.
Step 7: Use It with Quantities¶
my_scalar_type value{42.0};
auto length = value * m; // quantity<si::metre, my_scalar_type<double>>
auto area = length * length; // Quantities compose naturally
Validate with static_assert
Verify your type satisfies the expected concepts:
Practical Examples¶
Vector Representation¶
The library provides cartesian_vector
as a vector representation type with full support for vector operations:
#include <mp-units/cartesian_vector.h>
#include <mp-units/systems/si.h>
#include <mp-units/systems/isq.h>
using namespace mp_units;
using namespace mp_units::si::unit_symbols;
void example()
{
// Create 3D vector quantities
quantity_point pos1{cartesian_vector{1., 2., 3.} * m};
quantity_point pos2{cartesian_vector{4., 5., 6.} * m};
// Vector subtraction
quantity delta = pos2 - pos1; // {3, 3, 3} m
// Scalar multiplication
quantity delta_scaled = 2 * delta; // {6, 6, 6} m
// Magnitude (returns scalar quantity)
quantity distance = pos1.quantity_from_zero();
quantity mag = magnitude(distance); // sqrt(1² + 2² + 3²) m
// Scalar product (dot product)
auto dot = scalar_product(pos1, pos2); // Returns quantity with dimension m²
// Vector product (cross product)
auto cross = vector_product(pos1, pos2); // Returns vector quantity with dimension m²
}
The cartesian_vector implementation demonstrates how to create a full-featured vector type with:
- Arithmetic operations (
+,-,*,/) norm()member function (andmagnitude()alias)- Support for scalar and vector products
- Integration with quantity characters
Implementation reference:
cartesian_vector.h
Common Pitfalls¶
Provide value_type for Wrapper Types¶
The library needs to scale your type by a numeric factor during unit conversions. Make
sure wrapper types provide a value_type member to help determine the correct scaling
factor type:
template<typename T>
class my_wrapper {
public:
using value_type = T; // ✅ Helps library determine scaling factor type
// ...
};
See value_type or element_type
for complete details.
Place Customization Point Functions in the Same Namespace (ADL)¶
When providing customization point functions (like real(), imag(), norm()),
place them in the same namespace as your type to ensure Argument-Dependent Lookup (ADL)
finds them:
namespace my_namespace {
template<typename T>
class my_complex {
// ...
};
// ✅ Good: in the same namespace, found by ADL
template<typename T>
[[nodiscard]] constexpr T real(const my_complex<T>& c) { return c.get_real(); }
} // namespace my_namespace
Assuming Implicit Conversions that Are Actually Explicit¶
The default implicitly_scalable only allows implicit conversion when the unit ratio is a
non-truncating integer multiplier (or the target representation is floating-point). If you
expect implicit conversion between, say, mm and m with an integer representation, you'll
get a compilation error:
quantity<si::millimetre, int> a = 1000 * mm;
quantity<si::metre, int> b = a; // ❌ explicit conversion required (truncating: 1000 → 1)
quantity<si::metre, int> c = value_cast<si::metre>(a); // ✅ OK
This is intentional — the conversion is truncating. Specialize mp_units::implicitly_scalable
only when you are certain your type handles the conversion without data loss.
Summary¶
To create a custom representation type:
- Implement required operations for your character (scalar/complex/vector/tensor)
- Provide character-specific functions (if needed):
real(),imag(),norm(), etc., via member functions or ADL-findable free functions - Add formatting support (optional) via
std::formatter - Add
value_typeto help the library determine the scaling factor type - Specialize
representation_values<Rep>(if needed) for custom special values; optionally specializeconstraint_violation_handler<Rep>for custom error handling - Implement
operator*(T, representation_underlying_type_t<T>)andoperator/(T, representation_underlying_type_t<T>)so that scaling correctly updates all internal fields (e.g. forwith_variance<T>scalevaluebykandvariancebyk²) - Implement
operator*(T, UnitMagnitude)(optional) for magnitude-aware scaling when bounds or other type-level properties must change during unit conversion - Specialize
implicitly_scalable(if needed) to control implicit vs. explicit conversion semantics - Verify with concepts using
static_assert
The library handles the rest, providing strong type safety and dimensional analysis for your custom types.
See Also¶
User's Guide:
- Representation Types - Complete design and requirements reference
- Character of a Quantity - Understanding quantity characters
- Value Conversions - How
treat_as_floating_pointandimplicitly_scalableaffect conversions - Concepts - The
RepresentationOfconcept definition
How-to Guides:
- Ensure Ultimate Safety - Combining
constrainedreps withcheck_in_rangefor guaranteed bounds enforcement
Implementation References:
representation_concepts.h- Concept definitions and character-determination CPOs (disable_real,real,imag,modulus,norm,magnitude)customization_points.h- User-specializable customization points (representation_underlying_type,treat_as_floating_point,representation_values,constraint_violation_handler,quantity_like_traits,quantity_point_like_traits)quantity_traits.h- Public helpers (unit_for,reference_for,rep_for)scaling.h- Built-in scaling implementationvalue_cast.h-value_cast,force_in,is_integral_scaling, andimplicitly_scalablecartesian_vector.h- Vector implementation example